In this blog, we'll cover how to integrate hardware devices like USB card readers with Java web apps using the Web Serial API in a Vaadin app.
Many developers rule out web apps immediately when device integration is involved. But in many cases, it is for the wrong reasons. Technologies have changed, but for more than two decades, I have been involved in developing device integrations for various web applications. There are some compromises that need to be made, but often, there are bigger ones if one needs to develop a separate native application.
A prototype up and running. A Vaadin application running in Chrome ready to read data from electronic punch cards via the Emit 250 reader unit.
This summer, I volunteered as an organizer for the World Masters Orienteering Championships, which saw nearly 5,000 athletes participate, including those in the public races. Among them, almost 2,000 athletes used rental gear provided by Emit. I had already developed a Vaadin app to assist with various administrative tasks, but manually typing in the 6-digit serial numbers for the rental gear, both when issuing and returning it, was becoming a burden for us organizers. Recognizing an opportunity to improve the user experience, I added USB competition card reader support to our admin UI, streamlining the process significantly.
Before getting my hands dirty, I quickly Googled to see if anyone else had undertaken a similar effort. I discovered that the Emit 250 USB reader device functions as a serial port, which meant that the Web Serial API would be the appropriate tool to use. And that there had been a Norwegian fellow who had already done half of the job for me by publishing an open-source TypeScript library to parse the data from various Emit timing gear.
The Emit reading API implemented in TypeScript has an API to open the serial port and read the serial port data. When it gets a complete readout from a card, it exposes that data, like the card serial number and punch details. That was even more than I needed as I was only interested in the serial number reading.
Let’s check quickly how the Web Serial API can be used in a Java web application. The solution is now exposed as a Vaadin add-on, and the complete source code with an example view is available on GitHub.
Obtaining a SerialPort references
The Web Serial API is close relative to other hardware APIs introduced by Google’s Chrome team, like Web Bluetooth API and WebUSB API. Those are available nowadays in essentially all Chromium-based browsers. Requesting access to the serial port with navigator.serial.requestPort()
call is only allowed from a JS execution initiated by “user activation.” So you can’t request a SerialPort
reference from an onload event (but I checked later that requesting a port works fine from, e.g. a server side executeJS
call). If the port is already granted earlier (during the same browsing session) for the website, the SerialPort
reference can be obtained with navigator.serial.getPorts();
.
As I initially thought Chrome would block requestPort()
calls if initiated from a server-side event handler, my solution was to base the integration on a UI component, Emit250ReaderButton, that by default shows a button requesting a port with a JS event handler. In hindsight, I would write slightly less JS and call the connect250
method exposed from the emit-reading.ts file from a normal click listener. In the attached listener, the reconnect250
method checks for an already granted port and connects immediately if available.
getContent().getElement().executeJs("""
this.addEventListener('click', () => {
window.connect250();
});
""");
addAttachListener(e -> {
// Try to reconnect (existing grant to a port)
getContent().getElement().executeJs("""
window.reconnect250();
""");
The exciting part of the connect250()
function, which requests access to a port with the browser API, is declared like this:
port250 = await navigator.serial.requestPort({
filters: [{
usbVendorId: 0x0403,
usbProductId: 0x6001,
}]
});
This call makes the browser open a dialog listing all matching serial ports and lets the users either pick a port or cancel the operation. The filter parameter is optional, but as I knew exactly which kind of serial port was to be used, I could help the user pick the right one by filtering out all but the serial port type that the Emit 250 USB reader uses. In the screenshot below, you’ll see the dialog, without the filters, showing all serial devices seen by my laptop (I didn’t have a 250 device in my hand anymore when writing the article).
Pushing data to the Java side
Implementing the Vaadin API was then fairly straightforward. Once the port is obtained, the integration script utilizes the TypeScript library to parse the raw byte stream. The integration fires custom DOM events from relevant places, like when a connection is acquired and a complete competition card data is read. The Vaadin code then listens to these DOM events and exposes the same data as Java API.
The TypeScript code snippet exposing the competition card data as custom DOM events:
while (true) {
const { value, done } = await reader250.read();
if (done) {
console.log("[readLoop] DONE", done);
break;
}
if (value) {
console.log("250 value", value);
const readoutEvent = new CustomEvent("ecard-readout-event", { detail: JSON.stringify(value) });
document.body.dispatchEvent(readoutEvent);
}
}
On the Java side, DOM events are listened to and exposed through a Java API, as shown below. The EcardConsumer
, which serves as the Java API in this add-on, is essentially a java.util.function.Consumer
provided as a constructor parameter.
For more advanced usage, I suggest creating a custom interface to improve readability and documentation and support multiple listeners.
e.getUI().getElement().addEventListener("ecard-readout-event", e1 -> {
String jsonstr = e1.getEventData().getString("event.detail");
Ecard250Readout ecard250 = Ecard250Readout.fromJson(jsonstr);
ecardConsumer.accept(ecard250);
}).addEventData("event.detail");
Possible alternative approaches
In my earlier Web Bluetooth-based heart rate monitor example, I used quite a different approach. Instead of doing complex bit/byte level parsing of the data on the client side, I’m pushing the raw data as a byte stream to the server side. This makes it easier for me to handle the data with a programming language I master better. Unless there wouldn't have been this TypeScript library already available, this would have been my choice again.
The downside of this approach would have been that all raw data from the serial port would have been pushed to the server. With the 9600 baud rate, sending every bit to the server wouldn’t be an issue, but in theory, parsing the data early in the browser can be more optimal. For example, in this case (as we are parsing the data on the browser), we could have only posted the serial number of the competition card to the server and ignored the majority of the data.
How did it work out?
A fraction of the rental gear (electronic competition cards) was registered as returned with the Web Serial API-powered Vaadin app; it is waiting for cleanup and re-use.
It worked well! 😀 At our competition info desk, we had two laptops with the Emit 250 reader, which greatly helped our crew to keep track of who rented which card during the races. There were no typos by our crew as numbers were read in from the serial port. Also, those competitors who had bought their gear used the service at the information desk to register their new card numbers to our result system.
The most significant help was after the last competition when the rental gear was returned within about 3 hours. Rental returns were registered immediately in our system by flashing the card on the reader and checking from the screen which bucket it belonged to. Right after the competitions, we knew who to remind about the non-returned gear.
Now, on to you. What's your experience with integrating web apps and hardware devices? Have you faced any challenges or successes you'd like to share? Comment below, or start a conversation in the Vaadin Community Forum!