You are here

My first JavaFX application - 3 - Serial port

javaPrevious article

Let's now start exchanging data with the device we have to handle. Doing this requires that we receive and send some data over a serial port (or a virtual serial port over USB). This has always been a pain in the neck with Java. And Java 8 did not change this. We still have to install some separate piece of software, which depends on the platform where the application is being developed or run. The most frequently used solution is RXTX. And that's the one I use in my projects. 

To install it on my Linux PC, I adhere to following steps:

  • install package librxtx-java (version 2.2pre2-11 or more recent)
  • add /usr/share/java/RXTXcomm.jar external JAR to the Eclipse project
  • right-click on the jar in the list of referenced libraries in the Eclipse project, select Properties / Native Library and enter the directory containing native libraries: /usr/lib/jni

RXTX API documentation is in /usr/share/doc/librxtx-java/api while Java Communication API is here.

The first function to implement is one that returns a list of available serial ports. Then, this list must be displayed to the user, so that he can select the serial port he wants. But before implementing this function, that's good to add the capability to display log messages. The related UI part will be another list view, right below the list view displaying received frames. So, we will be able to display any problem to the user: it's always important to let the user know what's happening!

The DisplayMessage interface defines available display services for log messages. It is implemented by the main class. Two methods are available: display() and displayLater(). See further below for more information.

The FrameHandler class is in charge of providing functions allowing to drive serial ports. The function listing available ports is a method named getSerialPorts()FrameHandler implements the SerialPortEventListener interface: we conform to an event-driven paradigm to handle the serial port.

As most graphical user interface environments, JavaFX runs an event dispatch thread, and all modifications to the UI have to be performed from this thread. This means that we are not allowed, for instance, to display a message to the user from the serial port event listener. A simple solution exists: using the Platform.runLater() method. The implementation of DisplayMessage.displayLater() calls this method.

After those enhancements, the new version of the application lets the user choose a serial port, from among the existing ones, opens it, and displays a log message every time some bytes are received.

Associated source code is provided below.

Update - 03-Nov-2015 - As it can be seen in the code below, I started writing this application with an additional target in aim: to configure a Libelium's LoRa gateway in an easy way. Associated code is now available here.

UserInterface.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.paint.*?>
<?import javafx.scene.text.*?>

<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="583.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.40" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.monblocnotes.deviceController">
   <children>
      <GridPane>
        <columnConstraints>
          <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
          <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
        </columnConstraints>
        <rowConstraints>
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
        </rowConstraints>
         <children>
            <Label text="Select serial port device:" />
            <Button fx:id="sendReadCommandBtn" mnemonicParsing="false" text="Send READ command" GridPane.rowIndex="1" />
            <ComboBox fx:id="serialPortCB" prefHeight="26.0" prefWidth="307.0" GridPane.columnIndex="1" />
         </children>
      </GridPane>
      <ListView fx:id="recFramesLV" prefHeight="319.0" prefWidth="600.0" />
      <ListView fx:id="logMsgsLV" prefHeight="200.0" prefWidth="200.0" />
   </children>
</VBox>

UserInterfaceController.java

package com.monblocnotes.devicecontroller;

import java.net.URL;
import java.util.ArrayList;
import java.util.ResourceBundle;

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListView;

public class UserInterfaceController implements Initializable, EventHandler<ActionEvent> {
	
	private final static int MAX_NB_FRAMES = 7;
	private final static int MAX_NB_LOGMSGS = 5;
	
	@FXML private ComboBox<String> serialPortCB;
	@FXML private Button sendReadCommandBtn;
	@FXML private ListView<String> recFramesLV;
	@FXML private ListView<String> logMsgsLV;

	private ObservableList<String> displayedFrames;
	private ListViewMessages recFrames;
	
	private ObservableList<String> displayedLogMsgs;
	private ListViewMessages logMsgs;
	
	private ProcessAction processAction;
	
	/**
	 * Called by FXML loader.
	 */
	@Override
	public void initialize(URL location, ResourceBundle resources) {

		sendReadCommandBtn.setOnAction(new EventHandler<ActionEvent>() {
			
			@Override
			public void handle(ActionEvent event) {

				// TODO
				
			}
		});

		// To display latest received frames.
		displayedFrames = FXCollections.observableArrayList();
		recFramesLV.setItems(displayedFrames);
		recFrames = new ListViewMessages(displayedFrames, MAX_NB_FRAMES);
		
		// To display latest log messages.
		displayedLogMsgs = FXCollections.observableArrayList();
		logMsgsLV.setItems(displayedLogMsgs);
		logMsgs = new ListViewMessages(displayedLogMsgs, MAX_NB_LOGMSGS);
		
		// To get serial port selected by the user.
		serialPortCB.setOnAction(this);
		
	}
	
	/**
	 * 
	 * @param processAction
	 */
	public void setProcessAction(ProcessAction processAction) {
		
		this.processAction = processAction;
		
	}
	
	/**
	 * 
	 * @param frame
	 */
	public void displayFrame(String frame) {
		
		recFrames.addMessage(frame);
		
	}
	
	public void displayLogMsg(String logMsg) {
		
		logMsgs.addMessage(logMsg);
		
	}

	/**
	 * 
	 * @param portNameList
	 */
	public void displaySerialPorts(ArrayList<String> portNameList) {
		
		ObservableList<String> ol = FXCollections.observableArrayList();
		ol.setAll(portNameList);
		serialPortCB.setItems(ol);
		
	}

	/**
	 * For EventHandler<ActionEvent> interface.
	 * Is called only by the combo box, when the user selects a serial port.
	 */
	@Override
	public void handle(ActionEvent event) {
		
		if (processAction != null) {
			processAction.serialPortValue(serialPortCB.getValue());
		} else {
			displayLogMsg("internal error: processAction is null");
		}
		
	}
	
	/**
	 * How to process actions.
	 */
	public interface ProcessAction {
		
		public void serialPortValue(String serialPortName);
		
	}

}

ListViewMessages.java

package com.monblocnotes.devicecontroller;

import javafx.collections.ObservableList;

public class ListViewMessages {
	
	private ObservableList<String> messageList;
	private int maxNumberOfMessages;

	/**
	 * 
	 * @param frameList
	 */
	public ListViewMessages(ObservableList<String> frameList, int maxNumberOfMessages) {
		
		this.messageList = frameList;
		this.maxNumberOfMessages = maxNumberOfMessages;
		
	}
	
	/**
	 * 
	 * @param frame
	 */
	public void addMessage(String message) {
		
		int s = messageList.size();
		if (s >= maxNumberOfMessages) {
			// Remove oldest element.
			messageList.remove(s - 1);
		}
		// Add new element.
		messageList.add(0, message);
		
	}

}

FrameHandler.java

package com.monblocnotes.devicecontroller;

import java.util.ArrayList;
import java.util.Enumeration;
import java.util.TooManyListenersException;

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;

public class FrameHandler implements SerialPortEventListener {
	
	private final static int PORT_OPEN_WAIT = 1000;
	// Characteristics of Libelium LoRa gateway serial port.
	private final static int PORT_SPEED = 38400;
	private final static int PORT_DATA_BITS = SerialPort.DATABITS_8;
	private final static int PORT_STOP_BITS = SerialPort.STOPBITS_1;
	private final static int PORT_PARITY = SerialPort.PARITY_NONE;
	private final static int PORT_FLOW_CONTROL = SerialPort.FLOWCONTROL_NONE;
	
	private SerialPort serialPort;
	private DisplayMessage displayMessage;
	
	/**
	 * 
	 * @param displayMessage
	 */
	public FrameHandler(DisplayMessage displayMessage) {
		
		this.displayMessage = displayMessage;
	}
	
	/**
	 * 
	 * @return
	 */
	public ArrayList<String> getSerialPorts() {
		
		ArrayList<String> portNameList = new ArrayList<String>();
		@SuppressWarnings("unchecked")
		Enumeration<CommPortIdentifier> portList = CommPortIdentifier.getPortIdentifiers();
		if (portList == null) {
			displayMessage.display("no CommPortIdentifier!");
			return null;
		}
		CommPortIdentifier port;
		while (portList.hasMoreElements()) {
			port = portList.nextElement();
			if (port.getPortType() != CommPortIdentifier.PORT_SERIAL) {
				displayMessage.display(port.getName() + " not a serial port");
				continue;
			}
			if (port.isCurrentlyOwned()) {
				displayMessage.display(port.getName() + " currently owned");
				continue;
			}
			portNameList.add(port.getName());
			displayMessage.display(port.getName() + " added to list");
		}
		if (portNameList.isEmpty()) {
			return null;
		}
		return portNameList;
	}
	
	/**
	 * Tries to open the serial port.
	 * @param serialPortName
	 * @return -1 - no such port
	 *         -2 - port in use
	 *         -3 - internal error
	 *         -4 - too many event listeners
	 *          0 - success
	 */
	public int setSerialPort(String serialPortName) {
		
		CommPortIdentifier commPortIdentifier;
		try {
			commPortIdentifier = CommPortIdentifier.getPortIdentifier(serialPortName);
		} catch (NoSuchPortException e) {
			displayMessage.display(serialPortName + " does not exist");
			return -1;
		}
		try {
			serialPort = (SerialPort) commPortIdentifier.open("FrameHandler", PORT_OPEN_WAIT);
		} catch (PortInUseException e) {
			displayMessage.display(serialPortName + " is in use");
			return -2;
		}
		try {
			serialPort.setSerialPortParams(PORT_SPEED, PORT_DATA_BITS, PORT_STOP_BITS, PORT_PARITY);
		} catch (UnsupportedCommOperationException e) {
			displayMessage.display("internal error: bad port profile");
			return -3;
		}
		try {
			serialPort.setFlowControlMode(PORT_FLOW_CONTROL);
		} catch (UnsupportedCommOperationException e) {
			displayMessage.display("internal error: bad flow control");
			return -3;
		}
		try {
			serialPort.addEventListener(this);
			serialPort.notifyOnDataAvailable(true);
		} catch (TooManyListenersException e) {
			displayMessage.display("too many event listeners");
			return -4;
		}
		displayMessage.display(serialPortName + " opened and configured");
		return 0;
	}
	
	/**
	 * Event notification must be enabled for every event in setSerialPort() above.
	 * We can't call displayMessage.display() from this method, as 
	 * @param event
	 */
	@Override
	public void serialEvent(SerialPortEvent event) {
		
		switch(event.getEventType()) {
		case SerialPortEvent.BI:
		case SerialPortEvent.CD:
		case SerialPortEvent.CTS:
		case SerialPortEvent.DSR:
		case SerialPortEvent.FE:
		case SerialPortEvent.OE:
		case SerialPortEvent.OUTPUT_BUFFER_EMPTY:
		case SerialPortEvent.PE:
		case SerialPortEvent.RI:
			break;
		case SerialPortEvent.DATA_AVAILABLE:
			displayMessage.displayLater("data available");
			break;
		default:
			displayMessage.displayLater("FrameHandler.serialEvent() - unknown event type: " + event.getEventType());
		}
		
	}
	
}

DisplayMessage.java

package com.monblocnotes.devicecontroller;

/**
 *
 * Interface used to specify display services provided by main class.
 *
 */
public interface DisplayMessage {

	/**
	 * Must be called from FX application context.
	 * @param message
	 */
	public void display(String message);
	
	/**
	 * Can be called from any context.
	 * @param message
	 */
	public void displayLater(String message);
	
}

Main.java

package com.monblocnotes.devicecontroller;
	
import java.util.ArrayList;

import com.monblocnotes.devicecontroller.UserInterfaceController.ProcessAction;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.stage.Stage;
import javafx.scene.Parent;
import javafx.scene.Scene;

public class Main extends Application implements DisplayMessage, ProcessAction {
	
	private UserInterfaceController controller;
	private FrameHandler frameHandler;
	
	/**
	 * 
	 */
	@Override
	public void start(Stage primaryStage) {
		
		FXMLLoader fxmlLoader = null;
		try {
			fxmlLoader = new FXMLLoader();
			Parent root = fxmlLoader.load(getClass().getResource("UserInterface.fxml").openStream());
			Scene scene = new Scene(root,400,600);
			scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
			primaryStage.setScene(scene);
			primaryStage.show();
		} catch(Exception e) {
			e.printStackTrace();
		}
		// Display list of available serial ports.
		controller = (UserInterfaceController)fxmlLoader.getController();
		frameHandler = new FrameHandler(this);
		ArrayList<String> portNameList = frameHandler.getSerialPorts();
		if (portNameList != null) {
			controller.displaySerialPorts(portNameList);
		} else {
			controller.displayLogMsg("No serial port available");
		}
		controller.setProcessAction(this);
		
	}
	
	/**
	 * 
	 * @param args
	 */
	public static void main(String[] args) {
		
		launch(args);
		
	}

	/**
	 * For DisplayMessage interface.
	 * 
	 * @param message
	 */
	@Override
	public void display(String message) {
		
		controller.displayLogMsg(message);
		
	}

	/**
	 * For DisplayMessage interface.
	 * 
	 */
	@Override
	public void displayLater(String message) {
		
		Platform.runLater(new Runnable() {

			@Override
			public void run() {
				
				controller.displayLogMsg(message);
				
			}
			
		});
	}
	
	/**
	 * For ProcessAction interface.
	 * 
	 * @param serialPortName
	 */
	@Override
	public void serialPortValue(String serialPortName) {
		
		frameHandler.setSerialPort(serialPortName);
		
	}

}