Skip to content

Commit

Permalink
- refactoring to use jSerialComm instead of rxtx
Browse files Browse the repository at this point in the history
- adding reconnect support to serial port
  • Loading branch information
Pavel Chuchma authored and Pavel Chuchma committed May 21, 2023
1 parent 98a9a23 commit 0c3e94a
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 151 deletions.
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,3 @@ Supported operations:
* Tested with [this one](https://www.czc.cz/premiumcord-usb-usb2-0-na-rs485-adapter/80182/produkt).
* Originally tried with a one dollar device from Aliexpress, but it was not able to send commands. The most probably a
broken piece, it cost me a lot of evenings :-(
* [RXTX library](http://fizzed.com/oss/rxtx-for-java)
* Linux: install `librxtx-java`
* `sudo apt-get install librxtx-java`
* Windows: package `Windows-x64` from http://fizzed.com/oss/rxtx-for-java
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
version=0.2.0-SNAPSHOT
version=0.3.0-SNAPSHOT
8 changes: 3 additions & 5 deletions hvac-controller/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@ repositories {
// Use Maven Central for resolving dependencies.
mavenLocal()
mavenCentral()
maven {
url 'https://repo.opennms.org/maven2'
}
}

publishing {
Expand All @@ -36,7 +33,8 @@ java {
}

dependencies {
implementation 'ch.qos.logback:logback-classic:1.3.7'
implementation 'com.fazecast:jSerialComm:2.9.3'

testImplementation 'junit:junit:4.13.2'
implementation 'org.rxtx:rxtx:2.2pre2'
implementation "ch.qos.logback:logback-classic:1.2.10"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package org.chuma.hvaccontroller.device;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;

import com.fazecast.jSerialComm.SerialPort;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class AbstractSerialPortConnection {
static Logger log = LoggerFactory.getLogger(AbstractSerialPortConnection.class.getName());
protected SerialPort serialPort;
protected InputStream inputStream = null;
protected OutputStream outputStream = null;
protected final String portName;
protected boolean running = false;

public AbstractSerialPortConnection(String portName) {
this.portName = portName;
}

protected abstract void initializePort() throws IOException;

protected void openSerialPort() throws IOException {
log.debug("Opening serial port {}", portName);
String portList = String.join(", ", Arrays.stream(SerialPort.getCommPorts()).map(sp -> "[" + sp.getSystemPortPath() + "] " + sp.getDescriptivePortName()).toArray(CharSequence[]::new));
log.info("Existing serial ports: {}", portList);
if (serialPort != null) {
serialPort.closePort();
}

serialPort = SerialPort.getCommPort(portName);

if (!serialPort.openPort()) {
throw new IOException("Failed to open port " + portName + ", existing ports are: " + portList);
}
}

public void start() {
running = true;
new Thread(() -> {
log.debug("Starting serial port reader thread");
try {
while (running) {
while (inputStream == null) {
try {
openSerialPort();
initializePort();
inputStream = serialPort.getInputStream();
outputStream = serialPort.getOutputStream();
} catch (Exception e) {
log.error("Port initialization failed, going to sleep for a moment before retry", e);
try {
//noinspection BusyWait
Thread.sleep(10_000);
} catch (InterruptedException ignored) {
}
}
}

try {
readImpl();
} catch (IOException e) {
log.error("Read failed", e);
inputStream = null;
outputStream = null;
}
}
} finally {
log.debug("Serial port reader thread finished");
}
}, "ComRead-" + portName).start();
}

public void stop() {
running = false;
if (serialPort != null) {
serialPort.closePort();
serialPort = null;
inputStream = null;
outputStream = null;
log.debug("Serial port listener closed");
}
}

protected abstract void readImpl() throws IOException;
}
Original file line number Diff line number Diff line change
@@ -1,101 +1,75 @@
package org.chuma.hvaccontroller.device;

import gnu.io.CommPortIdentifier;
import gnu.io.SerialPort;
import org.chuma.hvaccontroller.IPacketSource;
import org.chuma.hvaccontroller.packet.PacketData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

import com.fazecast.jSerialComm.SerialPort;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.chuma.hvaccontroller.IPacketSource;
import org.chuma.hvaccontroller.packet.PacketData;

public class HvacConnector implements IPacketSource {
static Logger log = LoggerFactory.getLogger(HvacConnector.class.getName());
private final String portName;
private final AbstractSerialPortConnection serialPortConnection;
BlockingQueue<PacketData> packetDataQueue = new LinkedBlockingQueue<>();
boolean stopped = false;
PacketReader packetReader;
PacketSender packetSender;
PacketReader packetReader = new PacketReader();
PacketSender packetSender = new PacketSender();

public HvacConnector(String portName) {
this.portName = portName;
}
class SerialPortConnection extends AbstractSerialPortConnection {
public SerialPortConnection(String portName) {
super(portName);
}

private static CommPortIdentifier getSelectedPortId(String portName) throws IOException {
List<String> existingPortNames = new ArrayList<>();
//noinspection rawtypes
Enumeration portList = CommPortIdentifier.getPortIdentifiers();
while (portList.hasMoreElements()) {
CommPortIdentifier portId = (CommPortIdentifier) portList.nextElement();
if (portId.getPortType() == CommPortIdentifier.PORT_SERIAL) {
if (portId.getName().equals(portName)) {
log.debug("Port found: " + portId.getName());
return portId;
}
existingPortNames.add(portId.getName());
@Override
protected void initializePort() throws IOException {
if (serialPort.setComPortParameters(2400, 8, SerialPort.ONE_STOP_BIT, SerialPort.EVEN_PARITY)
&& serialPort.setComPortTimeouts(SerialPort.TIMEOUT_READ_BLOCKING,
3600_000, 0)
&& serialPort.setFlowControl(SerialPort.FLOW_CONTROL_DISABLED)
) {
log.debug("Serial port '[{}] {}' configured.", serialPort.getSystemPortPath(), serialPort.getDescriptivePortName());
} else {
throw new IOException("Failed to set configure port " + serialPort.getDescriptivePortName());
}
}

throw new IOException("Serial port '" + portName + "' not found. Available serial ports are: " + String.join(", ", existingPortNames));
}

private SerialPort openSerialPort() throws IOException {
CommPortIdentifier portId = getSelectedPortId(portName);
try {
SerialPort serialPort = (SerialPort) portId.open("SimpleReadApp", 2000);
serialPort.notifyOnDataAvailable(false);
serialPort.setSerialPortParams(2400,
SerialPort.DATABITS_8,
SerialPort.STOPBITS_1,
SerialPort.PARITY_EVEN);
serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_NONE);
serialPort.setOutputBufferSize(0);
serialPort.setInputBufferSize(0);
serialPort.enableReceiveTimeout(600_000);
return serialPort;
} catch (Exception e) {
throw new IOException("Failed to open serial port '" + portName + "'", e);
@Override
protected void readImpl() throws IOException {
PacketData receivedPacket = packetReader.readNext(inputStream);
packetSender.notifyPacketReceived(outputStream, receivedPacket);
packetDataQueue.add(receivedPacket);
}
}

public void sendData(PacketData p) {
packetSender.send(p);
public HvacConnector(String portName) {
serialPortConnection = new SerialPortConnection(portName);
}

public void startRead() throws IOException {
SerialPort serialPort = openSerialPort();
startRead(serialPort.getInputStream(), serialPort.getOutputStream());
/**
* For testing purposes only
*/
public HvacConnector(InputStream testInputStream, OutputStream testOutputStream) {
serialPortConnection = new SerialPortConnection("FAKE-PORT");
serialPortConnection.inputStream = testInputStream;
serialPortConnection.outputStream = testOutputStream;
}

public void startRead(InputStream inputStream, OutputStream outputStream) {
this.packetReader = new PacketReader(inputStream);
this.packetSender = new PacketSender(outputStream);
public void sendData(PacketData p) {
packetSender.send(p);
}

Thread thread = new Thread(() -> {
while (!stopped) {
try {
PacketData receivedPacket = packetReader.readNext();
packetSender.notifyPacketReceived(receivedPacket);
packetDataQueue.add(receivedPacket);
} catch (IOException e) {
log.error("Read failure", e);
}
}
}, "HvacListener");
thread.setPriority(Thread.MAX_PRIORITY);
thread.start();
public void startRead() {
serialPortConnection.start();
}

@Override
public void stopRead() {
stopped = true;
serialPortConnection.stop();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,17 @@ public ReceivedChar(int character, int readTime) {
}
}

private final InputStream inputStream;
private boolean stopped = false;
private ReceivedChar c;
int[] buff = new int[PacketData.PACKET_LENGTH];
static Logger log = LoggerFactory.getLogger(PacketReader.class.getName());

public PacketReader(InputStream inputStream) {
this.inputStream = inputStream;
}

public PacketData readNext() throws IOException {
c = waitForStartChar(c);
public PacketData readNext(InputStream inputStream) throws IOException {
c = waitForStartChar(inputStream, c);
int initialReadTime = c.readTime;
for (int i = 0; i < PacketData.PACKET_LENGTH - 1 && !stopped; i++) {
buff[i] = c.character;
c = readChar();
c = readChar(inputStream);
}
buff[PacketData.PACKET_LENGTH - 1] = c.character;

Expand All @@ -50,9 +45,9 @@ public void stop() {
stopped = true;
}

private ReceivedChar waitForStartChar(ReceivedChar c) throws IOException {
private ReceivedChar waitForStartChar(InputStream inputStream, ReceivedChar c) throws IOException {
if (c == null || c.character == PacketData.STOP_BYTE) {
c = readChar();
c = readChar(inputStream);
if (c.character == PacketData.START_BYTE) {
return c;
}
Expand All @@ -62,12 +57,12 @@ private ReceivedChar waitForStartChar(ReceivedChar c) throws IOException {
if (log.isDebugEnabled()) {
log.debug(String.format("Ignoring initial char %02X with read time: %d%n", c.character, c.readTime));
}
c = readChar();
c = readChar(inputStream);
}
return c;
}

private ReceivedChar readChar() throws IOException {
private ReceivedChar readChar(InputStream inputStream) throws IOException {
long startTime = System.currentTimeMillis();
int b = inputStream.read();
if (b < 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,12 @@ class PacketSender {
PacketData current;
boolean packetSent = false;
private int retryCount;
private final OutputStream outputStream;

public PacketSender(OutputStream outputStream) {
this.outputStream = outputStream;
}

void send(PacketData packet) {
sendQueue.add(packet);
}

void notifyPacketReceived(PacketData receivedPacket) {
void notifyPacketReceived(OutputStream outputStream, PacketData receivedPacket) {
if (packetSent) {
if (receivedPacket.command == PacketType.CMD_SET_RESPONSE
&& receivedPacket.from == current.to && receivedPacket.to == current.from
Expand Down Expand Up @@ -55,7 +50,7 @@ void notifyPacketReceived(PacketData receivedPacket) {

if (current != null) {
try {
sendDataImpl(current);
sendDataImpl(outputStream, current);
packetSent = true;
} catch (IOException e) {
e.printStackTrace();
Expand All @@ -64,7 +59,7 @@ void notifyPacketReceived(PacketData receivedPacket) {
}
}

void sendDataImpl(PacketData p) throws IOException {
void sendDataImpl(OutputStream outputStream, PacketData p) throws IOException {
try {
Thread.sleep(20);
for (int c : p.rawData) {
Expand Down
Loading

0 comments on commit 0c3e94a

Please sign in to comment.