Brewing Java with the Raspberry Pi
by Stephen Chin
Interact with a USB scale to accurately measure your coffee beans and brew the perfect cup.
Published May/June 2015
The Raspberry Pi comes preloaded with Java SE Embedded 8, so getting started with Java is as simple as typing the java
command at the prompt. However, the ability to run Java on your Raspberry Pi wouldn’t be complete without an application to help perfect your coffee-brewing skills. This article takes a scientific approach to coffee brewing in order to obtain the perfect “cup of Joe.”
Communicating with a USB Scale
In order to precisely measure weight for coffee brewing, we are going to use a USB shipping scale. USB communication is based on a series of pipes (logical channels) that are connected to the device via endpoints. Furthermore, these endpoints are grouped into a set of interfaces. The protocol used by most shipping scales is simple one-way communication where the current scale value is repeatedly broadcast out over a single endpoint on the first interface.
To use the scale, simply plug it into one of the host ports on your Raspberry Pi and turn it on. It consumes only 16 mA, so it can be safely powered right off the Raspberry Pi USB bus.
The vendorId
and productId
show up when you plug in the scale and run the dmesg
command. For the Dymo M10 scale they are 0x0922 and 0x8003, respectively. For its sister scale, the Dymo M25, they are 0x0922 and 0x8004, respectively. However, any compatible Stamps.com or DymoStamp scale should work fine as long as you change the vendorId
and productId
in the code.
For communication with the USB scale, we are going to use the usb4java
library. This is an open source, JSR 80–compliant implementation of the standard JavaX-USB specification that has support for ARM Linux distributions such as the Raspberry Pi. You can download the latest version of usb4java
from the project website.
Make sure to download both the core library as well as the javax extension. From the core distribution you need to grab the following JAR files:
usb4java-1.2.0.jar
libusb4java-1.2.0-linux-arm.jar
commons-lang3-3.2.1.jar
And you need these additional JAR files from the usb4java-javax
distribution:
usb-api-1.0.2.jar
usb4java-javax-1.2.0.jar
Place all these files in a new folder called lib
in your project directory, and add them to your project dependencies in your IDE of choice.
You also need to create a properties file in the root package that tells the JavaX-USB wrapper to use usb4java
as the implementation. To accomplish this, create a file called javax.usb.properties
in the root package (under src
), and give it the contents shown in Listing 1.
javax.usb.services = org.usb4java.javax.Services
|
Listing 1
To test the data returned from the scale, we are going to use a single-class implementation of the USB scale protocol that returns 60 data points and prints out the results on the command line. This will be the basis for our coffee-brewing application.
public static void main(String[] args) throws UsbException {
UsbScaleTest scale = UsbScaleTest.findScale();
scale.open();
try {
for (int i = 0; i < 60; i++) {
scale.syncSubmit();
}
} finally {
scale.close();
}
}
|
Listing 2
The main method for our application is shown in Listing 2 and can be summarized as follows:
- Find a connected USB scale.
- Open a connection to the scale.
- For 60 data points, submit a request to retrieve data.
- Close the scale.
To ensure the scale is closed properly even in the case of an error or exception, we are using a try finally
block.
Listing 3 shows the implementation of the first method, findScale
, which discovers Dymo M10 or M25 scales using the JavaX-USB API. This calls the findDevice
method, which contains code that traverses the USB device tree (see Listing 4).
public static UsbScaleTest findScale() throws UsbException {
UsbServices services = UsbHostManager.getUsbServices();
UsbHub rootHub = services.getRootUsbHub();
// Dymo M10 Scale:
UsbDevice device = findDevice(rootHub, (short) 0x0922,
(short) 0x8003);
// Dymo M25 Scale:
if (device == null) {
device = findDevice(rootHub, (short) 0x0922,
(short) 0x8004);
}
if (device == null) {
return null;
}
return new UsbScaleTest(device);
}
|
Listing 3
private static UsbDevice findDevice(UsbHub hub, short
vendorId, short productId) {
for (UsbDevice device :
(List) hub.getAttachedUsbDevices()) {
UsbDeviceDescriptor desc =
device.getUsbDeviceDescriptor();
if (desc.idVendor() == vendorId &&
desc.idProduct() == productId) {
return device;
}
if (device.isUsbHub()) {
device = findDevice((UsbHub) device,
vendorId, productId);
if (device != null) {
return device;
}
}
}
return null;
}
|
Listing 4
To read data from the scale, we need to connect to the correct interface and endpoint. Fortunately, the USB scale protocol is fairly simple, so you can simply grab the first interface and endpoint and then start listening to the data coming in from the scale directly, as shown in Listing 5.
private void open() throws UsbException {
UsbConfiguration configuration =
device.getActiveUsbConfiguration();
iface = configuration.getUsbInterface((byte) 0);
// this allows us to steal the lock from the kernel
iface.claim(usbInterface -> true);
final List endpoints = iface.getUsbEndpoints();
// there is only 1 endpoint
pipe = endpoints.get(0).getUsbPipe();
pipe.addUsbPipeListener(this);
pipe.open();
}
|
Listing 5
Notice that we had to use the claim
method that accepts a UsbInterfacePolicy
. This allows us to force the kernel to detach from the USB interface so we can claim it for our application.
The implementation of syncSubmit
is trivial, calling the same-named method on the UsbPipe
:
private void syncSubmit()
throws UsbException {
pipe.syncSubmit(data);
}
|
However, the real work happens in the callback. To receive the callback, our class needs to implement the UsbPipeListener class, which has two required methods. The first is dataEventOccurred
, which will be called as a result of invoking syncSubmit
and contain the data returned from the scale. The second is errorEventOccurred
, which will be invoked when there is a problem interfacing with the scale.
The data sent by these shipping scales is a simple byte array that contains six values. The protocol is not well documented, but it has been reverse-engineered by the open source community. The data is as follows:
- Byte 0—Unused
- Byte 1—Special flags: empty=2, overweight=6, negative=5 (The conditions indicated by overweight and negative are described later.)
- Byte 2—Unit of measure: grams=2, ounces=11
- Byte 3—Weight scale
- Byte 4—Base weight low- order byte
- Byte 5—Base weight high- order byte
Listing 6 shows the implementation of dataEventOccurred
, which takes apart the byte array returned and prints out a human-readable scale value to the command line.
@Override
public void dataEventOccurred(UsbPipeDataEvent upde) {
boolean empty = data[1] == 2;
boolean overweight = data[1] == 6;
boolean negative = data[1] == 5;
boolean grams = data[2] == 2;
int scalingFactor = data[3];
int weight = (data[4] & 0xFF) + (data[5] << 8);
if (empty) {
System.out.println("EMPTY");
} else if (overweight) {
System.out.println("OVERWEIGHT");
} else if (negative) {
System.out.println("NEGATIVE");
} else {
// Use String.format b/c printf causes problems on
// remote exec
System.out.println(String.format("Weight = %,.1f%s",
scaleWeight(weight, scalingFactor),
grams ? "g" : "oz"));
}
}
|
Listing 6
Besides doing a little bit of bit-shifting to get the weight magnitude, we also need to scale the weight by the scalingFactor
returned in byte 3, as shown in Listing 7.
private double scaleWeight(int weight, int scalingFactor) {
return weight * Math.pow(10, scalingFactor);
}
|
Listing 7
In the case of an error, the best we can do is log the error and continue (see Listing 8).
@Override
public void errorEventOccurred(UsbPipeErrorEvent upee) {
Logger.getLogger(UsbScaleTest.class.getName()).log(
Level.SEVERE, "Scale Error", upee);
}
|
Listing 8
And the last method to finish the scale test is the close
algorithm. This simply closes the pipe and interface cleanly so that the next application that tries to use the scale will be able to access it:
public void close() throws
UsbException {
pipe.close();
iface.release();
}
|
Read the full article at oracle.com/javamagazine The subscription to Java Magazine is free
About the Author
Stephen Chin is the lead Java community manager at Oracle, author of the upcoming Raspberry Pi with Java (McGraw-Hill), and coauthor of Pro JavaFX 8 (Apress, 2014). He is also a JavaOne content co-chair. Chin has presented keynotes at numerous Java conferences including JavaOne, where he is a four-time Rock Star Award recipient.