Java Communications Without JNI
I recently wrote about using the RXTX library to program an Arduino over the USB comms port. This sort of communication requires Java to native system access, and some don't like some of the complexities in getting RXTX to work. For instance, you need all of the right native libraries in the right place, and you need to set the library path on the Java command line just right. PureJavaComm by Spare Time Labs is a pure Java alternative to RXTX — thanks to Al Williams for pointing this out to me.
PureJavaComm's goal is to provide Java-only access to the comms port requiring only the Java Native Access (JNA) library. JNA is an open source project maintained by Tim Wall that allows you to write Java code only to call into native libraries (i.e., DLLs on Windows). Without JNA, you would have to write potentially complex Java Native Interface (JNI) code or other glue code to make this work. For instance, the following code sample from the JNA Wikipedia page works as-is on Windows, Linux, or OS X, to call the native C runtime printf
function:
import com.sun.jna.Library; import com.sun.jna.Native; import com.sun.jna.Platform; /** Simple example of native library declaration and usage. */ public class HelloWorld { public interface CLibrary extends Library { CLibrary INSTANCE = (CLibrary) Native.loadLibrary( (Platform.isWindows() ? "msvcrt" : "c"), CLibrary.class); void printf(String format, Object... args); } public static void main(String[] args) { CLibrary.INSTANCE.printf("Hello, World\n"); for (int i = 0; i < args.length; i++) { CLibrary.INSTANCE.printf("Argument %d: %s\n", i, args[i]); } } }
Let's take a look at how it works, and explore an example using it with an Arduino.
Inside JNA
JNA works similar to Microsoft's Platform Invocation Services, COM's IDispatch, or Java reflection/introspection. It implements a single small layer of JNI code that acts as a stub to call into any native library from your Java code. It runs on most platforms that support Java, maps all Java primitive types to native types, handles conversion between Java and C strings, supports unicode, variable arguments, Microsoft COM, and variable arguments. See the JNA project page for a complete list of features, but this is the bulk of them.
C pointers are mapped to Java through the use of arrays, as in this example:
// C method void fill_buffer(int *buf, int len); // Equivalent JNA mapping void fill_buffer(int[] buf, int len);
However, if you want the memory to be accessible beyond the local scope of a function call, you need to use a com.sun.jna.Memory
object:
private com.sun.jna.Memory buffer = new com.sun.jna.Memory(1024); private void doSomethingNative() { //... fill_buffer(buffer, 1024); }
Using PureJavaComm for Serial Port Communications
To begin with the PureJavaComm library, I downloaded the code from github, and opened the project in NetBeans, although it works in Eclipse as well. I chose the "Clean and build project" method, and NetBeans proceeded to use the Maven build scripts to download all the dependencies and then build the project seamlessly, and without issue.
To test it with an Arduino, I took the sample code and project from my previous blog on Arduino, and modified it slightly. First, instead of RXTX, I included only two JAR files in my build. The first is jna-4.0.0.jar, which is included with PureJavaComm in the lib directory. Second, I used the output of the PureJavaComm build, purejavacomm-0.0.21.jar, found in the bin directory of the project.
The updated Java-to-Arduino code is below. Here's a quick summary of the changes (marked in the comments as well):
- Marked by "IMPORTS" in the comments, import the purejavacomms jtermios packages
- Marked by "PORTS", I noticed a slight difference in the COMM port names returned
- Marked by "CONTAINS", the difference in port names required this change
- Marked by "FLOW-CONTROL", setting some flow control parameters are required
import java.io.BufferedReader; import java.io.InputStreamReader; import java.util.Enumeration; import purejavacomm.*; // IMPORTS import jtermios.Termios.*; public class ArduinoJavaComms implements SerialPortEventListener { SerialPort port = null; private String appName = getClass().getName(); private BufferedReader input = null; private static final int TIME_OUT = 1000; // Port open timeout private static final String PORT_NAMES[] = { // PORTS "tty.usbmodem", // Mac OS X // "usbdev", // Linux // "tty", // Linux // "serial", // Linux // "COM3", // Windows }; public static void main(String[] args) { ArduinoJavaComms lightSensor = new ArduinoJavaComms(); lightSensor.initialize(); } public void initialize() { try { CommPortIdentifier portid = null; Enumeration portEnum = CommPortIdentifier.getPortIdentifiers(); while (ported == null && portEnum.hasMoreElements()) { portid = (CommPortIdentifier)portEnum.nextElement(); if ( portid == null ) continue; System.out.println("Trying: " + portid.getName()); for ( String portName: PORT_NAMES ) { if ( portid.getName().equals(portName) || portid.getName().contains(portName)) { // CONTAINS port = (SerialPort) portid.open("ArduinoJavaComms", TIME_OUT); port.setFlowControlMode( SerialPort.FLOWCONTROL_XONXOFF_IN+ SerialPort.FLOWCONTROL_XONXOFF_OUT); // FLOW-CONTROL input = new BufferedReader( new InputStreamReader( port.getInputStream() )); System.out.println( "Connected on port: " + portid.getName() ); port.addEventListener(this); port.notifyOnDataAvailable(true); } } } while ( true) { try { Thread.sleep(100); } catch (Exception ex) { } } } catch (Exception e) { e.printStackTrace(); } } @Override public void serialEvent(SerialPortEvent event) { try { switch (event.getEventType() ) { case SerialPortEvent.DATA_AVAILABLE: String inputLine = input.readLine(); System.out.println(inputLine); break; default: break; } } catch (Exception e) { e.printStackTrace(); } } }
Running this project requires that you have the two JAR files mentioned above in the class path, and that's it. I began receiving events from my Arduino the first time I tried it.
Happy coding!
-EJB