Skip to content

Commit 4a07f2e

Browse files
committed
Improved serial port handling
By separated the connection management (connecting, reconnecting, and listening for disconnections) from command handling (sending data over the port) ConnectionManager: Handles connection and reconnection logic. This class listens for disconnection events and tries to reconnect based on exponential backoff. Event Notifications: It notifies listeners of disconnects and reconnects. Defined a ConnectionListener interface that notify on events such as disconnect and reconnect. CommandSender: Depends on ConnectionManager for the current connection state. It only sends commands if the port is connected. It listens for onReconnect events to resend the last command automatically after a reconnect.
1 parent d0164f4 commit 4a07f2e

File tree

8 files changed

+187
-57
lines changed

8 files changed

+187
-57
lines changed

build.gradle

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,24 @@ application {
88
}
99

1010
group = 'org.senegas'
11-
version = '1.0.2'
11+
version = '1.1.0'
1212

1313
repositories {
1414
mavenCentral()
1515
}
1616

1717
dependencies {
18-
testImplementation platform('org.junit:junit-bom:5.9.1')
19-
testImplementation 'org.junit.jupiter:junit-jupiter'
20-
2118
// https://mvnrepository.com/artifact/com.fazecast/jSerialComm
22-
implementation group: 'com.fazecast', name: 'jSerialComm', version: '2.10.4'
19+
implementation group: 'com.fazecast', name: 'jSerialComm', version: '2.11.0'
2320

2421
// https://mvnrepository.com/artifact/com.formdev/flatlaf
2522
implementation group: 'com.formdev', name: 'flatlaf', version: '3.2.5'
2623

2724
// https://mvnrepository.com/artifact/com.miglayout/miglayout-swing
2825
implementation group: 'com.miglayout', name: 'miglayout-swing', version: '11.1'
26+
27+
testImplementation platform('org.junit:junit-bom:5.9.1')
28+
testImplementation 'org.junit.jupiter:junit-jupiter'
2929
}
3030

3131
test {

src/main/java/org/senegas/trafficlight/TrafficLightApp.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public class TrafficLightApp {
1818
public static final String TITLE = "Traffic Light App";
1919
//TODO
2020
// // see https://stackoverflow.com/questions/33020069/how-to-get-version-attribute-from-a-gradle-build-to-be-included-in-runtime-swing
21-
public static final String VERSION = "1.0.2";
21+
public static final String VERSION = "1.1.0";
2222

2323
public static void main(String[] args) {
2424
EventQueue.invokeLater(() -> new TrafficLightApp().create());

src/main/java/org/senegas/trafficlight/serial/CommPort.java

Lines changed: 0 additions & 42 deletions
This file was deleted.

src/main/java/org/senegas/trafficlight/serial/CommPortSelector.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,9 @@ void dumpPorts() {
3030
}
3131
}
3232

33-
public CommPort select() throws CommPortException {
33+
public SerialPort select() throws CommPortException {
3434

3535
SerialPort[] commPorts = SerialPort.getCommPorts();
36-
3736
Optional<SerialPort> opt = Arrays.stream(commPorts)
3837
.filter(port -> port.getDescriptivePortName().contains("CH340"))
3938
.findFirst();
@@ -49,9 +48,9 @@ public CommPort select() throws CommPortException {
4948
throw new CommPortException("Could not find a connected Arduino");
5049
}
5150

52-
SerialPort port = opt.get();
53-
LOGGER.log(Level.INFO, "Arduino detected on port {0}", port.getSystemPortName());
51+
SerialPort serialPort = opt.get();
52+
LOGGER.log(Level.INFO, "Arduino detected on port {0}", serialPort.getSystemPortName());
5453

55-
return new CommPort(port);
54+
return serialPort;
5655
}
5756
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package org.senegas.trafficlight.serial;
2+
3+
import java.util.logging.Level;
4+
import java.util.logging.Logger;
5+
6+
public class CommandSender implements ConnectionListener {
7+
private static final Logger LOGGER = Logger.getLogger(CommandSender.class.getName());
8+
9+
private final ConnectionManager connectionManager;
10+
private String lastSentCommand;
11+
12+
public CommandSender(ConnectionManager connectionManager) {
13+
this.connectionManager = connectionManager;
14+
connectionManager.addConnectionListener(this);
15+
}
16+
17+
public void send(String command) {
18+
if (connectionManager.isConnected()) {
19+
int written = connectionManager.getSerialPort().writeBytes((command + "\0").getBytes(), command.length() + 1);
20+
if (written == -1) {
21+
LOGGER.log(Level.SEVERE, "There was an error writing to the port.");
22+
}
23+
lastSentCommand = command;
24+
}
25+
26+
}
27+
28+
@Override
29+
public void onDisconnect() {
30+
LOGGER.log(Level.SEVERE, "Disconnected, will attempt to resend last command on reconnect.");
31+
}
32+
33+
@Override
34+
public void onReconnect() {
35+
if (lastSentCommand != null) {
36+
LOGGER.log(Level.INFO, "Resend last command after reconnect.");
37+
try {
38+
LOGGER.log(Level.INFO, "Wait seconds in case the Arduino board is booting...");
39+
Thread.sleep(10_000);
40+
send(lastSentCommand); // Resend last command after reconnect
41+
LOGGER.log(Level.INFO, "Last command resent.");
42+
} catch (InterruptedException e) {
43+
Thread.currentThread().interrupt();
44+
}
45+
}
46+
}
47+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package org.senegas.trafficlight.serial;
2+
3+
public interface ConnectionListener {
4+
void onDisconnect();
5+
void onReconnect();
6+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package org.senegas.trafficlight.serial;
2+
3+
import com.fazecast.jSerialComm.SerialPort;
4+
import com.fazecast.jSerialComm.SerialPortDataListener;
5+
import com.fazecast.jSerialComm.SerialPortEvent;
6+
7+
import java.io.ByteArrayOutputStream;
8+
import java.util.ArrayList;
9+
import java.util.List;
10+
import java.util.logging.Level;
11+
import java.util.logging.Logger;
12+
13+
public class ConnectionManager {
14+
15+
private static final Logger LOGGER = Logger.getLogger(ConnectionManager.class.getName());
16+
17+
private SerialPort serialPort;
18+
private final int BASE_RECONNECT_INTERVAL = 5000; // 5 seconds
19+
private final int MAX_RECONNECT_INTERVAL = 60000; // 1 minute
20+
private final int MAX_RETRIES = 10; // Max retry limit
21+
22+
private final List<ConnectionListener> listeners = new ArrayList<>();
23+
24+
public ConnectionManager(SerialPort jSerialPort) {
25+
this.serialPort = jSerialPort;
26+
27+
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
28+
LOGGER.log(Level.INFO, "Closing port {0}...", jSerialPort.getSystemPortPath());
29+
boolean isPortClosed = jSerialPort.closePort();
30+
LOGGER.log(Level.INFO, "Closing port has {0}", (isPortClosed ? "been successful" : "failed"));
31+
}));
32+
33+
connect();
34+
}
35+
36+
public void addConnectionListener(ConnectionListener listener) {
37+
listeners.add(listener);
38+
}
39+
40+
private void notifyDisconnect() {
41+
for (ConnectionListener listener : listeners) {
42+
listener.onDisconnect();
43+
}
44+
}
45+
46+
private void notifyReconnect() {
47+
for (ConnectionListener listener : listeners) {
48+
listener.onReconnect();
49+
}
50+
}
51+
52+
public void connect() {
53+
if (serialPort.openPort()) {
54+
// https://fazecast.github.io/jSerialComm/javadoc/com/fazecast/jSerialComm/SerialPortDataListener.html
55+
// https://github.com/Fazecast/jSerialComm/wiki/Event-Based-Reading-Usage-Example
56+
serialPort.addDataListener(new SerialPortDataListener() {
57+
@Override
58+
public int getListeningEvents() {
59+
return SerialPort.LISTENING_EVENT_PORT_DISCONNECTED;
60+
}
61+
62+
@Override
63+
public void serialEvent(SerialPortEvent event) {
64+
switch (event.getEventType()) {
65+
// https://github.com/Fazecast/jSerialComm/wiki/Modes-of-Operation#for-port-disconnects
66+
case SerialPort.LISTENING_EVENT_PORT_DISCONNECTED:
67+
LOGGER.log(Level.INFO, "Port disconnected. Attempting to reconnect...");
68+
69+
handlePortDisconnection();
70+
break;
71+
}
72+
}
73+
});
74+
}
75+
}
76+
77+
public boolean isConnected() {
78+
return serialPort.isOpen();
79+
}
80+
81+
public SerialPort getSerialPort() {
82+
return this.serialPort;
83+
}
84+
85+
private void handlePortDisconnection() {
86+
serialPort.closePort();
87+
notifyDisconnect();
88+
int reconnectInterval = BASE_RECONNECT_INTERVAL;
89+
int attemptCount = 0;
90+
91+
while (! serialPort.isOpen() && attemptCount < MAX_RETRIES) {
92+
try {
93+
Thread.sleep(reconnectInterval);
94+
LOGGER.log(Level.INFO, "Attempting to reconnect...");
95+
if (serialPort.openPort()) {
96+
LOGGER.log(Level.INFO, "Reconnected successfully.");
97+
reconnectInterval = BASE_RECONNECT_INTERVAL; // Reset interval on success
98+
notifyReconnect();
99+
} else {
100+
reconnectInterval = Math.min(reconnectInterval * 2, MAX_RECONNECT_INTERVAL); // Exponential backoff
101+
LOGGER.log(Level.INFO, "Reconnection failed. Next attempt in " + reconnectInterval / 1000 + " seconds.");
102+
}
103+
} catch (InterruptedException e) {
104+
Thread.currentThread().interrupt();
105+
LOGGER.log(Level.SEVERE, "Reconnect attempt interrupted.", e);
106+
break;
107+
}
108+
attemptCount++;
109+
}
110+
111+
if (! serialPort.isOpen()) {
112+
LOGGER.log(Level.SEVERE, "Failed to reconnect after " + MAX_RETRIES + " attempts.");
113+
}
114+
}
115+
}

src/main/java/org/senegas/trafficlight/view/SerialMessageEmitter.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package org.senegas.trafficlight.view;
22

3+
import com.fazecast.jSerialComm.SerialPort;
34
import org.senegas.trafficlight.model.TrafficLightModel;
4-
import org.senegas.trafficlight.serial.CommPort;
5+
import org.senegas.trafficlight.serial.CommandSender;
6+
import org.senegas.trafficlight.serial.ConnectionManager;
57
import org.senegas.trafficlight.serial.CommPortException;
68
import org.senegas.trafficlight.serial.CommPortSelector;
79

@@ -13,7 +15,7 @@
1315
public class SerialMessageEmitter implements PropertyChangeListener {
1416
private static final Logger LOGGER = Logger.getLogger(SerialMessageEmitter.class.getName());
1517
private TrafficLightModel model;
16-
private CommPort port;
18+
private CommandSender commandSender;
1719

1820
public SerialMessageEmitter() {
1921
initSerialPort();
@@ -27,12 +29,15 @@ public void setModel(TrafficLightModel model) {
2729

2830
@Override
2931
public void propertyChange(PropertyChangeEvent evt) {
30-
this.port.send(this.model.toArduinoCommand());
32+
this.commandSender.send(this.model.toArduinoCommand());
3133
}
3234

3335
private void initSerialPort() {
3436
try {
35-
this.port = CommPortSelector.INSTANCE.select();
37+
SerialPort serialPort = CommPortSelector.INSTANCE.select();
38+
serialPort.setBaudRate(9600); // Set as needed
39+
40+
this.commandSender = new CommandSender(new ConnectionManager(serialPort));
3641
} catch (CommPortException e) {
3742
LOGGER.log(Level.SEVERE, e.getMessage());
3843
}

0 commit comments

Comments
 (0)