© Stephen Chin, Johan Vos and James Weaver 2019
S. Chin et al.The Definitive Guide to Modern Java Clients with JavaFXhttps://doi.org/10.1007/978-1-4842-4926-0_12

12. JavaFX 11 on Raspberry Pi

Stephen Chin1 , Johan Vos2 and James Weaver3
(1)
Belmont, CA, USA
(2)
Leuven, Belgium
(3)
Marion, IN, USA
 

Written by José Pereda

In this chapter, you will learn about how to get started with a Raspberry Pi device and about the required steps to run Java and JavaFX 11 applications, discussing the ways to do local or remote development and how to do remote deployment.

You will be presented different samples, starting from the very basic Java and JavaFX applications, and finally you will be shown a more complex project that tries to create a homemade in-car navigation system, with the help of a GPS device.

Intro to Raspberry Pi

Raspberry Pi and Arduino are the cornerstones of the Maker movement that has been going on for more than 10 years. But these are also the foundation of the Internet of Things (IoT) that has been taking off for years not only for hobbyists but in many industrial sectors. And they even reach more relevance in the STEAM initiative (for Science, Technology, Engineering, Art, and Mathematics), which targets directly the education of our children.

In fact, the Raspberry Pi was born as a small, inexpensive computer intended to be used by kids to learn programming in the school at early stages. As the Raspberry Pi Foundation states (www.raspberrypi.org):

Our mission is to put the power of computing and digital making into the hands of people all over the world.

As proof of that, the usual distributions for Raspberry Pi come with Scratch, Python, or Java preinstalled. By 2017, in only 6 years, more than 17 million units have been sold, where 5 million of these correspond to the Raspberry Pi 3 Model B, only in 2017.

Whether you are a hobbyist, you work on professional IoT projects, or you have children who want to learn computing, there are many reasons why you should consider doing a very small investment in a Raspberry Pi.

This chapter will give you a brief introduction on getting started with it and programming and running Java and JavaFX applications on this embedded device.

Getting Started with a Raspberry Pi

You can follow www.raspberrypi.org/documentation/ on how to get started.

Initial Kit

There, you will find what components are required, depending on your budget. The following are the minimum requirements to get started and complete the samples of this chapter.

Raspberry Pi

Buy a Raspberry Pi 3 Model B+1 if you haven’t done it yet. Its main specifications are as follows:
  • SoC: Broadcom BCM2837B0, Cortex-A53 (ARMv8), 64-bit SoC at 1.4 GHz

  • GPU: Broadcom Videocore-IV

  • RAM: 1 GB LPDDR2 SDRAM

  • Bluetooth: 2.4 GHz and 5 GHz IEEE 802.11.b/g/n/ac wireless LAN, Bluetooth 4.2

  • Networking: Gigabit Ethernet over USB 2.0

  • Graphics: H.264 MPEG-4 decode (1080P30); H.264 encode (1080P30); OpenGL ES 1.1,2.0 graphics

  • General-Purpose Input/Output (GPIO): Extended 40-pin GPIO header

  • Ports: 4 USB 2.0 ports and full-size HDMI; CSI camera port for connecting a Pi camera; DSI display port for connecting a Pi touchscreen display; 4-pole stereo output and composite video port

  • PoE: Power-over-Ethernet (PoE) support

Power Adaptor

You may buy it with a complete starting kit or select just the required accessories, including at least a 5 V micro-USB power adaptor supplying at least 2 A and an SD Card.

SD Card

Follow www.raspberrypi.org/documentation/setup/.
  • Choose an SD card of 8 or 16 GB. I’ll choose a SanDisk Ultra microSDHC 16 GB Class 10.

    There are SD cards with NOOBS preinstalled, but the images can be easily downloaded and installed as well.

  • Keyboard and mouse are optional. Both will require USB connection.

Monitor

You can use any monitor or TV display with HDMI connection, but there is a dedicated Raspberry Pi touch display monitor: www.raspberrypi.org/documentation/hardware/display/README.md.
  • It is a 7″ LCD display which connects to the Raspberry Pi through the DSI connector:

    www.raspberrypi.org/products/raspberry-pi-touch-display/

  • Resolution: The full color display outputs up to 800 × 480 (not that good compared to an HDMI connection) and features a capacitive touch sensing capable of detecting ten fingers.

  • It requires external power supply (using another micro-USB power supply is more convenient than connecting through the same Pi board).

  • A good case to mount both Raspberry Pi and display is convenient.

  • It requires to rotate the display 180° (see later in this chapter).

Install SD

Follow www.raspberrypi.org/documentation/installation/installing-images/README.md.

I’ll choose Raspbian Stretch2 with desktop image from www.raspberrypi.org/downloads/raspbian/. You can choose Lite or any other distribution of course.

In summary
$ diskutil unmount /dev/disk3s1
  • Copy the .img file:

    $ sudo dd if=/Users/JosePereda/Downloads/2018-11-13-raspbian-stretch.img of=/dev/rdisk3 bs=1m

  • It will take a few minutes, depending on the image file size. You can check the progress by sending a SIGINFO signal (press Ctrl-T).

  • When finished, unmount the SD card from your Mac and mount it on the Pi.
  • Download the Win32DiskImager utility from the SourceForge Project page: http://sourceforge.net/projects/win32diskimager/ as an installer file, and run it to install the software.

  • Run the Win32DiskImager utility from your desktop or menu.

  • Select the image file you extracted earlier.

  • In the device box, select the drive letter of the SD card. Be careful to select the correct drive: if you choose the wrong drive, you could destroy the data on your computer's hard disk! If you are using an SD card slot in your computer and can't see the drive in the Win32DiskImager window, try using an external SD adapter.

  • Click “Write” and wait for the write to complete.

  • Exit the imager and eject the SD card.

Raspberry Pi Configuration

See www.raspberrypi.org/documentation/configuration/.

Raspi-config

See www.raspberrypi.org/documentation/configuration/raspi-config.md.

The first time booting into Raspbian, raspi-config runs on your display. Later on, to open the configuration tool, simply run the following from the command line or via SSH (as explained in the following):
$ sudo raspi-config
../images/468104_1_En_12_Chapter/468104_1_En_12_Fig2_HTML.jpg
Figure 12-2

Running raspi-config

  • Change user password:
    • The default user on Raspbian is pi with the password raspberry. You can change that here. For convenience, I’ll set the same password: pi, which comes handy. But it is quite the opposite of a secure password, of course.

  • Network options:
    • Hostname is the visible name for this Pi on a network. Change the default raspberrypi, if required, for instance, if you have more than one device.

    • Wi-Fi: Enter SSID and passphrase. It allows connecting to a wireless network using the Raspberry Pi 3's built-in wireless connectivity and will let you work remotely on it from your usual development machine, using SSH.

  • Boot options:
    • Desktop/CLI: Use this option to change your boot preference to command line or desktop. We’ll choose command line, but desktop can be used too. You can choose autologin.

  • Localization options:
    • Choose from keyboard layout, time zone, locale, and Wi-Fi country code. All options on these menus default to British or GB until you change them.

  • Interfacing options:
    • Camera, if you have one.

    • SSH: Enable it; this is required to access remotely.

  • Advanced options:
    • Memory split: Set at least 256–512 MB for GPU if you are going to run JavaFX.

  • Update: Recommended, but it takes a while (only with the Pi connected to the network).

  • When finished, reboot.

Note that you can find directly the applied settings in the /boot/config.txt file.

After logging in again, you can check the Wi-Fi settings:
$ sudo nano /etc/wpa_supplicant/wpa_supplicant.conf
You can add as many SSIDs as you need. You can use
$ sudo iwlist wlan0 scan

to find the available networks at any given location.

Run ifconfig to find if your Pi is connected to the network. As you can see in Figure 12-3, wlan0 is connected, at a given local IP address, and data packages are being received and transmitted (RX, TX).
../images/468104_1_En_12_Chapter/468104_1_En_12_Fig3_HTML.jpg
Figure 12-3

Running ifconfig

By default, DHCP is used. If you need a static IP address, check www.raspberrypi.org/documentation/configuration/wireless/access-point.md and run
$ sudo nano /etc/dhcpcd.conf

to configure your eth0 or wlan0 static IP addresses.

In case you have the 7” Raspberry Pi display, you will need to rotate it 180°. Edit the config.txt file:
$ sudo nano /boot/config.txt
Add the following at the end of the file:
lcd_rotate=2

Then, save (Ctrl-O), and exit (Ctrl-X).

Finally, note that you should never power off your Raspberry Pi, by unplugging it from the power source while it is on, before shutting it down, to prevent the risk of damaging the file system. For a proper way to shut it down, run:
$ sudo shutdown –h now

Then wait a bit, and disconnect power.

Remote Connection via SSH

Most of the time, we will connect to the Raspberry Pi through SSH to run headlessly (i.e., without a dedicated monitor and keyboard on the Pi), having direct access from our developing machine (including copy/paste and file transfer options between both of them).

SSH is built into Linux distributions and Mac OS. For Windows and mobile devices, third-party SSH clients are available. On Linux and Mac OS, you can use SSH to connect to your Raspberry Pi from a Linux computer, a Mac, or another Raspberry Pi, without installing additional software. On Windows, the most commonly used client is called PuTTY and can be downloaded from greenend.​org.​uk. See www.raspberrypi.org/documentation/remote-access/ssh/windows.md.

Usually you will log in via
$ ssh pi@<IP>

where you need to supply the IP of the device. You can use hostname –I to find this IP if you are already connected to it, but if that’s not the case, you can try to find the device’s IP in the local network using nmap, or a mobile app, like Fing.

Usually the Raspberry Pi uses DHCP, which means it doesn’t have a fixed address, and after rebooting it can probably change. That is not convenient for an SSH connection. We can try to set a fixed IP, or we can also try to use its hostname to connect to it, providing it is broadcasted to the network. This works with Raspbian as it uses the multicast DNS protocol.
../images/468104_1_En_12_Chapter/468104_1_En_12_Fig4_HTML.jpg
Figure 12-4

Starting an SSH session

Since Mac OS and Linux use Bonjour, both support mDNS. On Windows, you can install Bonjour from here, https://support.apple.com/kb/DL999, for instance.

Then you can log in via hostname.local, in this case:

After you enter the password, you will have access to the Raspberry Pi (Figure 12-4). The first time you will see a security/authenticity warning. Type yes to continue.

Installing Java 11

Raspbian comes with Java installed. If you run java –version, it will print
java version "1.8.0_65"
Java(TM) SE Runtime Environment (build 1.8.0_65-b17)
Java HotSpot(TM) Client VM (build 25.65-b01, mixed mode)
However, this is a very outdated version.

We are going to install and use Java 11.

Java 11 for ARM

There are a number of options if you want to run Java on your Raspberry Pi. If you want to run a Java 11 distribution, completely based on OpenJDK, with integrated JIT (hotspot), you can use a build from https://adoptopenjdk.net.

To download JDK 11 for ARM, you can go to https://github.com/AdoptOpenJDK/openjdk11-binaries/releases/tag/jdk-11.0.4%2B11 and select the latest build for Linux ARM (32-bit version suitable for current versions of Raspbian and supported on Raspberry Pi 2 and Raspberry Pi 3) and install via SSH:
$ cd Downloads
$ wget https://github.com/AdoptOpenJDK/openjdk11-binaries/releases/download/jdk-11.0.4%2B11/OpenJDK11U-jdk_arm_linux_hotspot_11.0.4_11.tar.gz/ -O OpenJDK11U-jdk_arm_linux_hotspot_11.0.4_11.tar.gz
$ sudo tar -xvzf OpenJDK11U-jdk_arm_linux_hotspot_11.0.4_11.tar.gz -C /opt
$ sudo rm OpenJDK11U-jdk_arm_linux_hotspot_11.0.4_11.tar.gz
$ cd /opt
If you run now
$ jdk-11.0.4+11/bin/java -version
it should print
openjdk version "11.0.4" 2019-07-16
OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.4+11)
OpenJDK Server VM AdoptOpenJDK (build 11.0.4+11, mixed mode)
Let’s set now Java 11 as the default JVM. For that, we need to create the paths to the java and javac commands, so that they can be used anywhere:
$ sudo update-alternatives --install "/usr/bin/java" "java" "/opt/jdk-11.0.4+11/bin/java" 1
$ sudo update-alternatives --set java /opt/jdk-11.0.4+11/bin/java
$ sudo update-alternatives --install "/usr/bin/javac" "javac" "/opt/jdk-11.0.4+11/bin/javac" 1
$ sudo update-alternatives --set javac /opt/jdk-11.0.4+11/bin/javac
We will also add the JAVA_HOME environment variable and add it to the PATH:
$ nano /home/pi/.bashrc
Add at the end
export JAVA_HOME=/opt/jdk-11.0.4+11
export PATH=$PATH:$JAVA_HOME/bin
Save and exit (Ctrl-O, Ctrl-X), and reboot:
$ sudo reboot
Testing Java 11
Let’s test Java 11 and the new Launch Single-File Source-Code Programs feature:
$ cd /home/pi/
$ mkdir ModernClients
$ cd ModernClients
$ nano Test.java
Add a main method as shown in Listing 12-1 or copy the file from Sample0: https://github.com/modernclientjava/mcj-samples/tree/master/ch12-RaspberryPi/Sample0.
public class Test {
    public static void main(String... args) {
        System.out.println("Hello Java " +
             System.getProperty("java.version") + " for ARM!");
    }
}
Listing 12-1

Sample0

Save, exit (Ctrl-O, Ctrl-X), and run:
$ java Test.java
It should print the result of Figure 12-5.
../images/468104_1_En_12_Chapter/468104_1_En_12_Fig5_HTML.jpg
Figure 12-5

Running Java 11 on the Raspberry Pi

Congratulations on running your first Java 11 application on your brand-new Raspberry Pi! Now that Java has been installed successfully, you can move to the next step: installing JavaFX.

Installing JavaFX 11

JavaFX 11 builds for ARM 32 can be downloaded from https://gluonhq.com/products/javafx. This is 100% the same JavaFX sources that are used on desktop platforms (Windows, Mac OS, Linux), but with specific drivers for ARM and 32 bits. Note the regular JavaFX distribution for Linux won’t work, as it is built for x86_64 with 64 bits.

From an SSH session, download the SDK, move it to /opt, and unzip it:
$ wget -O armv6hf-sdk-11.0.2.zip https://gluonhq.com/download/javafx-11-0-2-sdk-armv6hf/
$ sudo mv armv6hf-sdk-11.0.2.zip /opt
$ cd /opt
$ sudo unzip armv6hf-sdk-11.0.2.zip
$ sudo rm armv6hf-sdk-11.0.2.zip

If you look at the list of files under the lib folder, you will find the jars for the different JavaFX modules, as long as the native libraries for ARM.

Note

While you will find the media and web JavaFX modules, these are not supported yet on ARM. Neither is Swing.

For convenience, let’s export PATH_TO_FX:
$ nano /home/pi/.bashrc
$ export PATH_TO_FX= /opt/armv6hf-sdk/lib

Running JavaFX Applications Locally

We can now try to run a HelloFX sample from https://github.com/modernclientjava/mcj-samples/tree/master/ch12-RaspberryPi/Sample1, which is based on the https://openjfx.io/openjfx-docs/ samples.

Listing 12-2 contains the code for a HelloFX java class that extends from the application JavaFX class.
package org.modernclients.raspberrypi;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class HelloFX extends Application {
    @Override
    public void start(Stage stage) {
        String javaVersion = System.getProperty("java.version");
        String javafxVersion = System.getProperty("javafx.version");
        Label label = new Label("Hello, JavaFX " + javafxVersion +
            ", running on Java " + javaVersion + ".");
        Scene scene = new Scene(new StackPane(label), 800, 480);
        stage.setScene(scene);
        stage.setTitle("Hello JavaFX");
        stage.show();
    }
    public static void main(String[] args) {
        launch(args);
    }
}
Listing 12-2

Sample1

To run it now, from an SSH session, let’s first clone the repository with the samples:
$ cd /home/pi/Downloads
$ wget https://github.com/modernclientjava/mcj-samples/archive/master.zip
$ unzip master.zip
$ mv mcj-samples-master /home/pi/ModernClients
And now enter the sample 1:
$ cd /home/pi/ModernClients/ch12-RaspberryPi/Sample1
$ javac --module-path $PATH_TO_FX --add-modules=javafx.controls
    src/org/modernclients/raspberrypi/HelloFX.java -d dist
$ sudo java --module-path $PATH_TO_FX --add-modules=javafx.controls
    -Dembedded=monocle -Dglass.platform=Monocle -cp dist/.
    org.modernclients.raspberrypi.HelloFX
The application will run, but it will be displayed only in a connected monitor. You can quit the application with Ctrl-C from the SSH terminal. Alternatively, you can also try to kill the Java processes:
$ sudo killall -9 java
../images/468104_1_En_12_Chapter/468104_1_En_12_Fig6_HTML.jpg
Figure 12-6

Running JavaFX 11 on the Raspberry Pi

JavaFX mouse events require write permissions to access the hardware, and that’s why we need to use sudo; otherwise, the application will start, but an exception will be printed to the console:
Udev: Failed to write to /sys/class/input/mice/uevent
Check that you have permission to access input devices
java.io.FileNotFoundException: /sys/class/input/mice/uevent (Permission denied)
    at java.base/java.io.FileOutputStream.open0(Native Method)
...
    at javafx.graphics/com.sun.glass.ui.monocle.SysFS.write(SysFS.java:121)
...
Now that we have the sample running, we can try to run it as well from an X11 session. From the Raspberry Pi now, we run startx; then we open a terminal and type
$ cd /home/pi/ModernClients/ch12-RaspberryPi/Sample1
$ sudo java --module-path $PATH_TO_FX --add-modules=javafx.controls
    -cp dist/. org.modernclients.raspberrypi.HelloFX
Since we have removed the monocle options, now we have a regular windowed application (Figure 12-7). And we can use the mouse to close it and stop the Java process.
../images/468104_1_En_12_Chapter/468104_1_En_12_Fig7_HTML.jpg
Figure 12-7

Running JavaFX on X11

Let’s now try to run a Gradle sample as well, in order to simplify the command line process:
$ cd /home/pi/ModernClients/ch12-RaspberryPi/Sample2/
$ nano gradlew
We need to add sudo to the last line:
exec sudo "$JAVACMD"...
Save and exit (Ctrl-O, Ctrl-X), and run
$ ./gradlew run
The first time it will download Gradle 5.0-zip, and it will create a Gradle deamon, so it can take a while before the process fails:
Loading library prism_es2 from resource failed: java.lang.UnsatisfiedLinkError: /root/.openjfx/cache/11.0.2/libprism_es2.so: /root/.openjfx/cache/11.0.2/libprism_es2.so: wrong ELF class: ELFCLASS64 (Possible cause: architecture word width mismatch)

The javafx-gradle-plugin only works on Desktop, and it defaults to Linux x86_64, so the native libraries are not compatible, as the Pi will require Linux ARM 32.

But still, we can use Gradle to compile and build the project and then use the local SDK to run it:
./gradlew build
sudo java --module-path $PATH_TO_FX:build/libs -Dembedded=monocle -Dglass.platform=Monocle -m hellofx/org.modernclients.raspberrypi.HelloFX
Note that we can create a Gradle task to avoid typing this on command line all the time. Edit the build.gradle file and add Listing 12-3 at the end.
def javaHome = '/opt/jdk-11.0.4+11'
def javafxHome = '/opt/armv6hf-sdk'
task runLocalEmbedded(type: Exec) {
    dependsOn 'build'
    commandLine 'sudo', "${javaHome}/bin/java", '-Dfile.encoding=UTF-8', '--module-path', "${javafxHome}/lib:${buildDir}/libs", '-Dembedded=monocle', '-Dglass.platform=Monocle', '-m', "${project.mainClassName}"
}
Listing 12-3

Sample2 build.gradle file

Save (Ctrl+O, Ctrl+X), and now run:
$ ./gradlew runLocalEmbedded
Press Ctrl-C to quit the app. Note that sometimes the app doesn’t close, because there are still some Gradle deamon threads running. You can stop them by finding the ID of the Java process(es)
$ ps –a
$ sudo kill <pid of Java process>
or directly with
$ sudo killall -9 java

Running JavaFX Applications Remotely

While these projects are compiled and built locally on the Raspberry Pi, it is quite slower compared to building on your machine, and the lack of IDE or the inconveniences of development over SSH invite to look for a different approach: develop on your regular machine and then deploy and run on the Pi.

On the other hand, development on our machine is way faster, but then we still have the deployment issue: we’ll need to copy the related files of our application to the Raspberry Pi, before we can run on it.

There are a few options to copy the required files, like the classic FTP or even SCP (a command for sending files over SSH). This means you can copy files between computers, either from your Raspberry Pi to your desktop or laptop or the other way around.

For instance, let’s say we have the Sample3: modernclientjava/mcj-samples/tree/master/ch12-RaspberryPi/Sample3. Using JDK 11. We compile it and build it with Maven on our machine, and then we copy the resulting classes to the Raspberry Pi:
$ cd mcj-samples-master/ch12-RaspberryPi/Sample3
$ mvn clean compile
$ cd ..
$ scp -r Sample3 [email protected]:/home/pi/ModernClients/ch12-RaspberryPi/Sample3
(add password)
Now, we can run from the SSH terminal:
$ cd /home/pi/ModernClients/ch12-RaspberryPi/Sample3/target
$ sudo java --module-path $PATH_TO_FX:classes -Dembedded=monocle
    -Dglass.platform=Monocle
    -m hellofx/org.modernclients.raspberrypi.MainApp

While this works, it is a tedious error-prone manual process, and it would be better if we could have this step integrated within our IDE, or we could have a plugin for our build tool.

Let’s examine some options.

Java Remote Platform

NetBeans created a while ago the concept of remote platform. You can define the settings of a JVM on another machine and use an Ant task to deploy and run over SSH on that machine.

This comes very handy of course in the case of the Raspberry Pi.

To install Apache NetBeans 11.1, go to https://netbeans.apache.org/download/nb111/nb111.html and choose the installer for you platform.

Once installed, go to ToolsJava Platforms. Click Add Platform... and select Remote Java Standard Edition.

Provide your details: name of the remote platform, like Pi 11, and host: It can be raspberrypi.local; user, pi; password; and remote JRE path, /opt/jdk-11.0.1+13. See Figure 12-8.
../images/468104_1_En_12_Chapter/468104_1_En_12_Fig8_HTML.jpg
Figure 12-8

Remote platform configuration

When the remote platform has been created, make sure you add sudo to the exec prefix, as in Figure 12-9.

Finally, with the Raspberry Pi available, click Test Platform and see that the test is successful. Otherwise, make sure all the fields are correctly set.

Let’s try the remote platform with an example. Follow the instructions here, https://openjfx.io/openjfx-docs/#IDE-NetBeans, to create a new Java application without build tools, or download Sample4 from https://github.com/modernclientjava/mcj-samples/tree/master/ch12-RaspberryPi/Sample4.

First of all, make sure the app runs fine on your machine.

Now on NetBeans, edit Properties, select RunRuntime Platform, and pick Pi11. Provide a configuration name like Pi11. Make sure you provide the path to the JavaFX SDK and include the monocle options in the VM options, as in Figure 12-10,
--module-path /opt/armv6hf-sdk/lib --add-modules=javafx.controls -Dembedded=monocle -Dglass.platform=Monocle
and close the dialog.
../images/468104_1_En_12_Chapter/468104_1_En_12_Fig9_HTML.jpg
Figure 12-9

Adding exec prefix to the remote platform

../images/468104_1_En_12_Chapter/468104_1_En_12_Fig10_HTML.jpg
Figure 12-10

Set project properties to run with a remote platform

When running the same application on desktop or on the Raspberry Pi, it can be convenient to adapt its window size based on the platforms it is run on, as shown in Listing 12-4.
String platform = System.getProperty("glass.platform");
Rectangle2D bounds;
if ("Monocle".equals(platform)) {
    bounds = Screen.getPrimary().getBounds();
} else {
    bounds = new Rectangle2D(0, 0, 600, 400);
}
Scene scene = new Scene(
    new StackPane(label), bounds.getWidth(), bounds.getHeight());
Listing 12-4

Configure window size based on platform

Now run again, from the Pi11 configuration. You will see the connection details in the NetBeans output window:
Connecting to raspberrypi.local:22
cmd : mkdir -p '/home/pi/NetBeansProjects//Sample4/dist'
Connecting to raspberrypi.local:22
done.
profile-rp-calibrate-passwd:
Connecting to raspberrypi.local:22
cmd : cd '/home/pi/NetBeansProjects//Sample4'; 'sudo' '/opt/jdk-11.0.4+11/bin/java'  -Dfile.encoding=UTF-8 --module-path /opt/armv6hf-sdk/lib --add-modules=javafx.controls -Dembedded=monocle -Dglass.platform=Monocle  -jar /home/pi/NetBeansProjects//Sample4/dist/HelloFX11.jar

You will see your app running nicely on the Pi’s display, while you can see the output from the process in the NetBeans output window. And you can even debug the application.

However, this approach has a few problems: it only works on NetBeans, and it is not valid for Maven or Gradle projects.

Gradle SSH Plugin

Another option is the SSH Gradle plugin from https://gradle-ssh-plugin.github.io. It will work on Gradle projects from terminal or any IDE with Gradle support.

Let’s run now this sample, https://github.com/modernclientjava/mcj-samples/tree/master/ch12-RaspberryPi/Sample5, from IntelliJ (or any IDE of your choosing).

Edit the build.gradle file, and verify the required configuration: working dir, Java home, JavaFX path, and your remote configuration (host, user, and password), as in Listing 12-5.
plugins {
    id 'application'
    id 'org.openjfx.javafxplugin' version '0.0.8'
    id 'org.hidetake.ssh' version '2.10.1'
}
repositories {
    mavenCentral()
}
javafx {
    modules = [ 'javafx.controls', 'javafx.fxml' ]
}
mainClassName = "$moduleName/org.modernclients.raspberrypi.MainApp"
def workingDir = '/home/pi/ModernClients/ch12-RaspberryPi/
def javaHome = '/opt/jdk-11.0.4+11'
def javafxHome = '/opt/armv6hf-sdk'
task libs(type: Copy) {
    dependsOn 'jar'
    into "${buildDir}/libs/"
    from configurations.runtime
}
remotes {
    pi11 {
        host = 'raspberrypi.local'
        user = 'pi'
        password = 'pi'
    }
}
task runRemoteEmbedded {
    dependsOn 'libs'
    ssh.settings {
        knownHosts = allowAnyHosts
    }
    doLast {
        ssh.run {
            session(remotes.pi11) {
                execute "mkdir -p ${workingDir}/${project.name}/dist"
                fileTree("${buildDir}/libs")
                        .filter { it.isFile() && ! it.name.startsWith('javafx')}
                        .files
                        .each { put from:it, into: "${workingDir}/${project.name}/dist/${it.name}"}
                executeSudo "${javaHome}/bin/java -Dfile.encoding=UTF-8 " +
                        "--module-path=${javafxHome}/lib:${workingDir}/${project.name}/dist " +
                        "-Dembedded=monocle -Dglass.platform=Monocle " +
                        "-classpath '${workingDir}/${project.name}/dist/*' " +
                        "-m ${project.mainClassName}"
            }
        }
    }
}
Listing 12-5

Gradle build file for Sample5

Note For convenience, the task sets allowAnyHosts, and host key checking is turned off. It will print a warning message that the process is vulnerable to man-in-the-middle attacks and it is not recommended for production.

With this plugin, hitting Ctrl-C from the terminal just kills the Gradle process, but not the application. To solve this issue, make sure you add an Exit button to the UI.

Run the runRemoteEmbedded task from your IDE Gradle’s window, like in Figure 12-11, or run from a terminal
$ ./gradlew runRemoteEmbedded
The app will be built, deployed to the Pi, and executed on it, and you will get the output from the process in your terminal, like in Figure 12-11.
../images/468104_1_En_12_Chapter/468104_1_En_12_Fig11_HTML.jpg
Figure 12-11

Executing the runRemoteEmbedded task

Working with Dependencies

So far, we have seen very simple use cases that were helpful to getting us started and setting everything properly.

Time now to have a look at a more complex example.

The DIY In-Car Navigation System

The following project is a Proof of Concept of a Do It Yourself homemade in-car navigation system. For that, we are going to install a GPS to the Raspberry Pi. A JavaFX application will display a map, and the GPS readings will be used to center the map in our current location.

Bill of Materials

Raspberry Pi 3 Model B+

7″ display 800 × 480; cage is optional but recommended.

5 V power adaptor for Raspberry Pi and display. Power bank is optional but recommended for field testing.

GPS: Universal Asynchronous Receiver-Transmitter (UART) Serial GPS Neo-7M (micro-USB is optional) (Figure 12-12), for instance, this one: http://wiki.keyestudio.com/index.php/KS0319_keyestudio_GPS_Module.

Four female-female jumper wires for GPIO connection.

Micro-USB: USB adaptor (in case the GPS breakout mounts micro-USB) is optional.

Antenna for the GPS is optional (but when used, the capacitor C2 has to be removed).
../images/468104_1_En_12_Chapter/468104_1_En_12_Fig12_HTML.jpg
Figure 12-12

UART Serial GPS Neo-7M. Image from http://wiki.keyestudio.com/File:KS0319.png

Setup for GPIO

We are going to use the General-Purpose Input/Output (GPIO) pins to get serial readings from the GPS.

The Raspberry Pi serial port consists of two signals, a transmit signal (TxD) and a receive signal (RxD), that are available at pins 8 and 10 on the 3 Model B+ (equivalent to GPIO #15 and #16 from Figure 12-13, in that order).
../images/468104_1_En_12_Chapter/468104_1_En_12_Fig13_HTML.jpg
Figure 12-13

Raspberry Pi 3 Model B+ GPIO pinout. Image from http://pi4j.com/pins/model-3b-plus-rev1.html

By default, the serial port on the Raspberry Pi is configured as a console port for communicating with the Linux OS shell. In order to access the serial port from a software program, we have to configure it. Open an SSH session, and run
$ sudo raspi-config

Select Interfacing options and now select Serial.

Now you have to select No, to disable the login shell access to serial, and then select Yes to enable the hardware serial port (or UART for Universal Asynchronous Receiver-Transmitter). Accept and reboot your Raspberry Pi.

GPIO Connections
The GPS module requires four connections that can be made using four jumper female-female wires; see Figure 12-14, from right to left:
  • VCC pin connected to pin 2 (power 5 V), red jumper

  • GND pin connected to pin 6 (ground), yellow jumper

  • Rx pin connected to pin 8 (TxD UART, GPIO 15), blue jumper

  • Tx pin connected to pin 10 (RxD UART, GPIO 16), green jumper

../images/468104_1_En_12_Chapter/468104_1_En_12_Fig14_HTML.jpg
Figure 12-14

GPS and Raspberry Pi 3 Model B+ GPIO connection

Note that a breakout board and pin ribbon cable can be used instead, extending the GPIO pins to a breadboard, where the connection to the GPS can be done more easily.

Required GPS Software
We need to install the following software from a terminal:
$ sudo apt-get install gpsd gpsd-clients
where gpsd is the interface daemon for GPS receivers. When finished, if the GPS is already attached, you can start reading it from the lowest-numbered serial port with
$ gpsd /dev/ttyS0
or in case you have connected the USB instead with
gpsd /dev/ttyUSB0
The best option to launch the gpsd is with this service:
$ sudo service gpsd start
Once the service has stared, you can verify its status with
$ sudo systemctl status gpsd.socket
that will display something like shown in Figure 12-15.
../images/468104_1_En_12_Chapter/468104_1_En_12_Fig15_HTML.jpg
Figure 12-15

Gpsd service active and running

If required, you can modify the default settings by editing the file:
$ sudo nano /etc/default/gpsd
Once everything is running properly, you can start a GPS monitor with
cgps /dev/ttyS0
or with
gpsmon /dev/ttyS0

If you are indoors, it is probably that the GPS won’t be able to connect to any satellite, and you won’t receive any value. But you will still get some readings.

If you take your Raspberry Pi outdoors, as long as the Wi-Fi connection holds, you can still be connected through SSH to your machine and visualize these readings and get something like shown in Figure 12-16.
../images/468104_1_En_12_Chapter/468104_1_En_12_Fig16_HTML.jpg
Figure 12-16

Gpsd service active and running

NMEA Readings
NMEA is an acronym for the National Marine Electronics Association , and the NMEA 0183 is a standard data format supported by all GPS manufacturers that uses an ASCII serial communication protocol. There are different message types or sentences, and all of them start with a header $GP and a code for the sentence like GLL that stands for geographic position, latitude, longitude, ending with * and a checksum. A possible message will look like
$GPGLL,5139.69658,N,00947.18207,W,200557.00,A,A*72

To find out about all the possible sentences and how to parse them, you can see this link: http://aprs.gids.nl/nmea/.

Listing 12-6 shows the model class we are going to use in our application to keep track of a few of the variables coming from the GPS, like latitude, longitude, altitude, or number of satellites, and Listing 12-7 shows a possible parser of the most significant NMEA messages like GPRMC or GPGGA.
package org.modernclients.raspberrypi.gps.model;
import javafx.beans.property.*;
public class GPSPosition {
    // time
    private final FloatProperty time = new SimpleFloatProperty(this, "time");
    public final FloatProperty timeProperty() { return time; }
    // getter & setter
    // latitude
    private final FloatProperty latitude = new SimpleFloatProperty(this,
        "latitude");
    public final FloatProperty latitudeProperty() { return latitude; }
    // getter & setter
    // longitude
    private final FloatProperty longitude = new SimpleFloatProperty(this,
        "longitude");
    public final FloatProperty longitudeProperty() { return longitude; }
    // getter & setter
    // direction
    private final FloatProperty direction = new SimpleFloatProperty(this,
        "direction");
    public final FloatProperty directionProperty() { return direction; }
    // getter & setter
    // altitude
    private final FloatProperty altitude = new SimpleFloatProperty(this,
        "altitude");
    public final FloatProperty altitudeProperty() { return altitude; }
    // getter & setter
    // velocity
    private final FloatProperty velocity = new SimpleFloatProperty(this,
        "velocity");
    public final FloatProperty velocityProperty() { return velocity; }
    // getter & setter
    // satellites
    private final IntegerProperty satellites = new SimpleIntegerProperty(this,
        "satellites");
    public final IntegerProperty satellitesProperty() { return satellites; }
    // getter & setter
    // quality
    private final IntegerProperty quality = new SimpleIntegerProperty(this,
         "quality");
    public final IntegerProperty qualityProperty() { return quality; }
    // getter & setter
    // fixed
    private final BooleanProperty fixed = new SimpleBooleanProperty(this,
        "fixed");
    public final BooleanProperty fixedProperty() { return fixed; }
    // getter & setter
    public void updatefix() {
        fixed.set(quality.get() > 0);
    }
    @Override
    public String toString() {
        return "GPSPosition{" +
                "time=" + time.get() +
                ", latitude=" + latitude.get() +
                ", longitude=" + longitude.get() +
                ", direction=" + direction.get() +
                ", altitude=" + altitude.get() +
                ", velocity=" + velocity.get() +
                ", quality=" + quality.get() +
                ", satellites =" + satellites.get() +
                ", fixed=" + fixed.get() +
                '}';
    }
}
Listing 12-6

GPSPosition class

package org.modernclients.raspberrypi.gps.service;
import javafx.beans.property.FloatProperty;
import javafx.beans.property.Property;
import org.modernclients.raspberrypi.gps.model.GPSPosition;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import java.util.logging.Logger;
public class NMEAParser {
    private static final Logger logger =
        Logger.getLogger(NMEAParser.class.getName());
    interface SentenceParser {
        boolean parse(String [] tokens, GPSPosition position);
    }
    private static final Map<String, SentenceParser> sentenceParsers =
        new HashMap<>();
    private final GPSPosition position;
    public NMEAParser(GPSPosition position) {
        this.position = position;
        sentenceParsers.put("GPGGA", new GPGGA());
        sentenceParsers.put("GPGGL", new GPGGL());
        sentenceParsers.put("GPRMC", new GPRMC());
        sentenceParsers.put("GPRMZ", new GPRMZ());
        sentenceParsers.put("GPVTG", new GPVTG());
    }
    public GPSPosition parse(final String line) {
        if (line.startsWith("$") && checksum(line)) {
            String[] tokens = line.substring(1).split(",");
            String type = tokens[0];
            if (sentenceParsers.containsKey(type)) {
                sentenceParsers.get(type).parse(tokens, position);
            }
            position.updatefix();
        }
        return position;
    }
    // parsers
    class GPGGA implements SentenceParser {
        @Override
        public boolean parse(String [] tokens, GPSPosition position) {
            parseCoordinate(tokens[2], tokens[3], "S",
                position.latitudeProperty());
            parseCoordinate(tokens[4], tokens[5], "W",
                position.longitudeProperty());
            doParse(tokens[1], Float::parseFloat, position.timeProperty());
            doParse(tokens[6], Integer::parseInt, position.qualityProperty());
            doParse(tokens[7], Integer::parseInt, position.satellitesProperty());
            return doParse(tokens[9], Float::parseFloat,
                position.altitudeProperty());
        }
    }
    class GPGGL implements SentenceParser {
        @Override
        public boolean parse(String [] tokens, GPSPosition position) {
            parseCoordinate(tokens[1], tokens[2], "S",
                position.latitudeProperty());
            parseCoordinate(tokens[3], tokens[4], "W",
                position.longitudeProperty());
            return doParse(tokens[5], Float::parseFloat, position.timeProperty());
        }
    }
    class GPRMC implements SentenceParser {
        @Override
        public boolean parse(String [] tokens, GPSPosition position) {
            doParse(tokens[1], Float::parseFloat, position.timeProperty());
            parseCoordinate(tokens[3], tokens[4], "S",
                position.latitudeProperty());
            parseCoordinate(tokens[5], tokens[6], "W",
                position.longitudeProperty());
            doParse(tokens[7], Float::parseFloat, position.velocityProperty());
            return doParse(tokens[8], Float::parseFloat,
                position.directionProperty());
        }
    }
    class GPVTG implements SentenceParser {
        @Override
        public boolean parse(String [] tokens, GPSPosition position) {
            return doParse(tokens[3], Float::parseFloat,
                position.directionProperty());
        }
    }
    class GPRMZ implements SentenceParser {
        @Override
        public boolean parse(String [] tokens, GPSPosition position) {
            return doParse(tokens[1], Float::parseFloat,
                position.altitudeProperty());
        }
    }
    private boolean parseCoordinate(String token, String direction, String
        defaultDirection, FloatProperty property) {
        if (token == null || token.isEmpty() || direction == null ||
            direction.isEmpty()) {
            return false;
        }
        int minutesPosition = token.indexOf('.') - 2;
        if (minutesPosition < 0) {
            return false;
        }
        float minutes = Float.parseFloat(token.substring(minutesPosition));
        float decimalDegrees = Float.parseFloat(token.substring(minutesPosition))
            / 60.0f;
        float degree = Float.parseFloat(token) - minutes;
        float wholeDegrees = (int) degree / 100;
        float coordinateDegrees = wholeDegrees + decimalDegrees;
        if (direction.startsWith(defaultDirection)) {
            coordinateDegrees = -coordinateDegrees;
        }
        property.setValue(coordinateDegrees);
        return true;
    }
    private <T> boolean doParse(String token, Function<String, T> operator,
        Property<T> property) {
        if (token == null || token.isEmpty()) {
            return false;
        }
        try {
            property.setValue(operator.apply(token));
            return true;
        } catch (NumberFormatException nfe) { }
        return false;
    }
    private static boolean checksum(String line) {
        if (line == null || ! line.contains("$") || ! line.contains("*")) {
            return false;
        }
        String sentence = line.substring(1, line.lastIndexOf("*"));
        String lineChecksum = "0x" + line.substring(line.lastIndexOf("*") + 1);
        int c = 0;
        for (char s : sentence.toCharArray()) {
            c ^= s;
        }
        String hex = String.format("0x%02X", c);
        boolean result = hex.equals(lineChecksum);
        if (! result) {
            logger.warning("There was an error in the checksum of " + line);
        }
        return result;
    }
}
Listing 12-7

NMEAParser class

GPIO and Java
Pi4J is a Java library that can be used to access the GPIO pins of the Raspberry Pi. As you can read at http://pi4j.com/:

It is a friendly object-oriented I/O API and implementation libraries for Java Programmers to access the full I/O capabilities of the Raspberry Pi platform. This project abstracts the low-level native integration and interrupt monitoring to enable Java programmers to focus on implementing their application business logic.

We are going to use its latest version available, 1.2, so we’ll need to include the dependency in our build:
dependencies {
    implementation 'com.pi4j:pi4j-core:1.2'
}
With Pi4J, creating a Serial object is as easy as
Serial serial = SerialFactory.createInstance();

Then we can add a listener to it so we can react to any incoming serial events, and we can configure the serial based on the usual settings.

Note that the library only works on the Raspberry Pi, but it can be used and compiled on your machine.

Listing 12-8 shows the service class that opens the serial port and starts listening to the serial events, extracting one by one all the sentences received from the gpsd process.
package org.modernclients.raspberrypi.gps.service;
import com.pi4j.io.gpio.GpioController;
import com.pi4j.io.gpio.GpioFactory;
import com.pi4j.io.serial.*;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import org.modernclients.raspberrypi.gps.model.GPSPosition;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.logging.Logger;
public class GPSService {
    private static final Logger logger =
        Logger.getLogger(GPSService.class.getName());
    @Inject
    private GPSPosition gpsPosition;
    private Serial serial;
    private GpioController gpio;
    private NMEAParser nmea;
    private StringBuilder gpsOutput;
    private final StringProperty line = new SimpleStringProperty();
    @PostConstruct
    private void postConstruct() {
        if (! "monocle".equals(System.getProperty("embedded"))) {
            return;
        }
        nmea = new NMEAParser(gpsPosition);
        gpsOutput = new StringBuilder();
        gpio = GpioFactory.getInstance();
        serial = SerialFactory.createInstance();
        serial.addListener(event -> {
            try {
                String s = event.getString(Charset.defaultCharset())
                        .replaceAll(" ", "")
                        .replaceAll(" ", "");
                gpsOutput.append(s);
                processReading();
            } catch (IOException e) {
                logger.warning("Error processing event " + event);
                e.printStackTrace();
            }
        });
        SerialConfig config = new SerialConfig();
        try {
            String defaultPort = SerialPort.getDefaultPort();
            logger.info("Connecting to default port = " + defaultPort);
            config.device(defaultPort)
                    .baud(Baud._9600)
                    .dataBits(DataBits._8)
                    .parity(Parity.NONE)
                    .stopBits(StopBits._1)
                    .flowControl(FlowControl.NONE);
            serial.open(config);
            logger.info("Connected: " + serial.isOpen());
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
    private void processReading() {
        if (gpsOutput == null || gpsOutput.toString().isEmpty()) {
            return;
        }
        String reading = gpsOutput.toString().trim();
        if (! reading.contains("$")) {
            return;
        }
        String[] split = reading.split("\$");
        for (int i = 0; i < split.length - 1; i++) {
            String line = "$" + split[i];
            gpsOutput.delete(0, line.length());
            if (line.length() > 1) {
                logger.fine("GPS: " + line);
                Platform.runLater(() -> {
                    nmea.parse(line);
                    this.line.set(line);
                });
            }
            if (i == split.length - 2) {
                gpsOutput.insert(0, "$");
            }
        }
    }
    public final StringProperty lineProperty() {
        return line;
    }
    public void stop() {
        logger.info("Stopping Serial and GPIO");
        if (serial != null) {
            try {
                serial.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (gpio != null) {
            gpio.shutdown();
        }
    }
}
Listing 12-8

GPSService class

While reading from a serial port, we have to be aware that we have a continuous flow of bytes, so we have to take care of converting them properly to string and taking out each sentence. That’s what the processReading method does, with the help of a single StringBuilder.

Also, notice that this thread runs in the background, so whenever there is a new sentence, we’ll use Platform::runLater to use it with the JavaFX properties, on the JavaFX application thread.

For every sentence, we’ll call the NMEA parser and update the GPSPosition object with the new values.

The UI

Let’s define now the JavaFX interface: we are going to display a map that will be centered into the latitude and longitude coordinates retrieved from the GPS reading.

Gluon Maps

Gluon Maps (https://gluonhq.com/labs/maps/) is an open source JavaFX 11 library that provides a map viewer component, rendering tile-based maps from OpenStreetMap. The project is available here: https://github.com/gluonhq/maps.

We can add a MapView container to the center of our view and use a MapLayer to render our position. On mobile devices with built-in GPS sensor, we could use the Gluon Attach position service, but on the Raspberry Pi (or any desktop machine with a connected GPS sensor), we can use the GPSService listed in the preceding text.

To add the map, we need the following dependencies:
repositories {
    mavenCentral()
    maven {
        url 'http://nexus.gluonhq.com/nexus/content/repositories/releases/'
    }
}
dependencies {
    implementation 'com.gluonhq:maps:2.0.0-ea+2'
    implementation 'com.gluonhq.attach:storage:4.0.2:desktop'
    implementation 'com.gluonhq.attach:util:4.0.2'
}
For convenience, we will define a PoiLayer that can place JavaFX nodes on top of the base map that will be our Points of Interest, based on latitude and longitude (Listing 12-9).
package org.modernclients.raspberrypi.gps.view;
import com.gluonhq.maps.MapLayer;
import com.gluonhq.maps.MapPoint;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.util.Pair;
public class PoiLayer extends MapLayer {
    private final ObservableList<Pair<MapPoint, Node>> points;
    public PoiLayer() {
        points = FXCollections.observableArrayList();
    }
    public void addPoint(MapPoint p, Node icon) {
        points.add(new Pair(p, icon));
        this.getChildren().add(icon);
        this.markDirty();
    }
    @Override
    protected void layoutLayer() {
        for (Pair<MapPoint, Node> candidate : points) {
            MapPoint point = candidate.getKey();
            Node icon = candidate.getValue();
            Point2D mapPoint = getMapPoint(point.getLatitude(),
                point.getLongitude());
            icon.setVisible(true);
            icon.setTranslateX(mapPoint.getX());
            icon.setTranslateY(mapPoint.getY());
        }
    }
}
Listing 12-9

PoiLayer class

Afterburner
Afterburner is a convenient minimalistic MVP framework based on Convention over Configuration and Dependency Injection from Adam Bien: https://github.com/AdamBien/afterburner.fx. To use it we need
repositories {
    mavenCentral()
}
dependencies {
    implementation 'com.airhacks:afterburner.fx:1.7.0'
    implementation 'javax.annotation:javax.annotation-api:1.3.2'
}
Scene Builder

Finally, we’ll use Scene Builder 11.0.0 from https://gluonhq.com/products/scene-builder/ to design the UI with FXML on our machine.

It is convenient to import Gluon Maps into Scene Builder custom controls library (Figure 12-17).
../images/468104_1_En_12_Chapter/468104_1_En_12_Fig17_HTML.jpg
Figure 12-17

Importing Gluon Maps into Scene Builder

Then we can create a new FXML file, with a top BorderPane container, and drag and drop the required components: a toolbar on top, the MapView on the center, a VBox with labels on the right to display the current GPSPosition values, and a ListView to display the NMEA sentences at the bottom (Figure 12-18).
../images/468104_1_En_12_Chapter/468104_1_En_12_Fig18_HTML.jpg
Figure 12-18

Designing the UI in Scene Builder

Note that with Afterburner, we will create the following files:

Java classes:
  • org.modernclients.raspberrypi.gps.view.UIView (Listing 12-10) that extends from FXMLView, a convenient container that takes care of loading the FXLM, CSS, or properties files by convention

  • org.modernclients.raspberrypi.gps.view.UIPresenter (Listing 12-14)

Resource files:
  • org.modernclients.raspberrypi.gps.view.ui.fxml (Listing 12-11)

  • org.modernclients.raspberrypi.gps.view.ui.css (Listing 12-12)

  • org.modernclients.raspberrypi.gps.view.ui.properties (Listing 12-13)

package org.modernclients.raspberrypi.gps.view;
import com.airhacks.afterburner.views.FXMLView;
import java.util.ResourceBundle;
public class UIView extends FXMLView {
    public UIView() {
        this.bundle = ResourceBundle.getBundle("/org/modernclients/view/ui");
    }
}
Listing 12-10

UIView class

<?xml version="1.0" encoding="UTF-8"?>
<?import com.gluonhq.maps.MapView?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.control.Separator?>
<?import javafx.scene.control.ToggleButton?>
<?import javafx.scene.control.ToolBar?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.layout.VBox?>
<BorderPane fx:id="pane" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.modernclients.raspberrypi.gps.view.UIPresenter">
   <bottom>
      <ListView fx:id="listView" maxHeight="200.0" BorderPane.alignment="CENTER" />
   </bottom>
   <right>
      <VBox prefHeight="250.0" prefWidth="200.0" styleClass="box" BorderPane.alignment="CENTER">
         <children>
            <VBox>
               <children>
                  <Label styleClass="gps" text="%label.time" />
                  <Label fx:id="timeLabel" styleClass="gps-data" />
               </children>
            </VBox>
            <VBox>
               <children>
                  <Label styleClass="gps" text="%label.position" />
                  <Label fx:id="positionLabel" styleClass="gps-data" />
               </children>
            </VBox>
            <VBox>
               <children>
                  <Label styleClass="gps" text="%label.altitude" />
                  <Label fx:id="altitudeLabel" styleClass="gps-data" />
               </children>
            </VBox>
            <VBox>
               <children>
                  <Label styleClass="gps" text="%label.direction" />
                  <Label fx:id="directionLabel" styleClass="gps-data" />
               </children>
            </VBox>
            <VBox layoutX="10.0" layoutY="112.0">
               <children>
                  <Label styleClass="gps" text="%label.speed" />
                  <Label fx:id="speedLabel" styleClass="gps-data" />
               </children>
            </VBox>
            <VBox layoutX="10.0" layoutY="146.0">
               <children>
                  <Label styleClass="gps" text="%label.quality" />
                  <Label fx:id="qualityLabel" styleClass="gps-data" />
               </children>
            </VBox>
            <VBox layoutX="10.0" layoutY="146.0">
               <children>
                  <Label styleClass="gps" text="%label.satellites" />
                  <Label fx:id="satellitesLabel" styleClass="gps-data" />
               </children>
            </VBox>
         </children>
      </VBox>
   </right>
   <top>
      <ToolBar BorderPane.alignment="CENTER">
         <items>
            <Label fx:id="statusLabel" styleClass="gps-data" text="%label.gps" />
            <Pane maxWidth="1.7976931348623157E308" prefWidth="200.0" />
            <Separator orientation="VERTICAL" />
            <ToggleButton fx:id="showLog" mnemonicParsing="false" text="%button.show.log" />
            <Separator layoutX="324.0" layoutY="10.0" orientation="VERTICAL" />
            <Button mnemonicParsing="false" onAction="#onZoomIn" text="%button.zoom.in" />
            <Button layoutX="10.0" layoutY="10.0" mnemonicParsing="false" onAction="#onZoomOut" text="%button.zoom.out" />
            <Separator layoutX="440.0" layoutY="10.0" orientation="VERTICAL" />
            <Button layoutX="20.0" layoutY="20.0" mnemonicParsing="false" onAction="#onExit" text="%button.exit" />
         </items>
         <padding>
            <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
         </padding>
      </ToolBar>
   </top>
   <center>
      <MapView fx:id="mapView" BorderPane.alignment="CENTER" />
   </center>
</BorderPane>
Listing 12-11

ui.fxml file

.box {
    -fx-padding: 20;
    -fx-spacing: 10;
    -fx-border-color: darkgray;
    -fx-border-width: 0 0 0 1;
}
.label.gps-data {
    -fx-text-fill: blue;
    -fx-font-size: 1.1em;
}
.label.gps {
    -fx-text-fill: darkgray;
    -fx-font-size: 1.0em;
}
Listing 12-12

ui.css file

button.show.log=Show Log
button.zoom.in=+
button.zoom.out=-
button.exit=Exit
label.time=Time
label.position=Position
label.altitude=Altitude
label.direction=Direction
label.speed=Speed
label.quality=Quality
label.satellites=Number of Satellites
label.gps=GPS Status: {0}
label.gps.fixed=fixed
label.gps.not-fixed=not fixed
Listing 12-13

ui.properties file

Once we have all these files, it is time to add now the presenter (Listing 12-14).
package org.modernclients.raspberrypi.gps.view;
import com.gluonhq.maps.MapPoint;
import com.gluonhq.maps.MapView;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import org.modernclients.raspberrypi.gps.model.GPSPosition;
import org.modernclients.raspberrypi.gps.service.GPSService;
import javax.inject.Inject;
import java.text.MessageFormat;
import java.util.ResourceBundle;
import java.util.logging.Logger;
public class UIPresenter {
    private static final Logger logger = Logger.getLogger(UIPresenter.class.getName());
    @FXML private BorderPane pane;
    @FXML private Label statusLabel;
    @FXML private MapView mapView;
    @FXML private ListView<String> listView;
    @FXML private Label timeLabel;
    @FXML private Label positionLabel;
    @FXML private Label altitudeLabel;
    @FXML private Label directionLabel;
    @FXML private Label speedLabel;
    @FXML private Label qualityLabel;
    @FXML private Label satellitesLabel;
    @FXML private ToggleButton showLog;
    @FXML private ResourceBundle resources;
    @Inject private GPSService service;
    @Inject private GPSPosition gpsPosition;
    private MapPoint mapPoint;
    public void initialize() {
        logger.info("Platform: " + System.getProperty("embedded"));
        mapView = new MapView();
        mapPoint = new MapPoint(50.0d, 4.0d);
        mapView.setCenter(mapPoint);
        mapView.setZoom(15);
        PoiLayer poiLayer = new PoiLayer();
        poiLayer.addPoint(mapPoint, new Circle(7, Color.RED));
        mapView.addLayer(poiLayer);
        pane.setCenter(mapView);
        service.lineProperty().addListener((obs, ov, nv) -> {
            logger.fine(nv);
            listView.getItems().add(nv);
            listView.scrollTo(listView.getItems().size() - 1);
            if (listView.getItems().size() > 100) {
                listView.getItems().remove(0);
            }
        });
        gpsPosition.timeProperty().addListener((obs, ov, nv) -> {
            statusLabel.setText(
               MessageFormat.format(resources.getString("label.gps"),
                  gpsPosition.isFixed() ? resources.getString("label.gps.fixed") :
                  resources.getString("label.gps.not-fixed")));
            mapPoint.update(gpsPosition.getLatitude(),
                 gpsPosition.getLongitude());
            mapView.setCenter(mapPoint);
        });
        timeLabel.textProperty().bind(Bindings.createStringBinding(() -> {
            float time = gpsPosition.getTime();
            int hour = (int) (time / 10000f);
            int min = (int) ((time - hour * 10000) / 100f);
            int sec = (int) (time - hour * 10000 - min * 100);
            return String.format("%02d:%02d:%02d UTC", hour, min, sec);
        }, gpsPosition.timeProperty()));
        positionLabel.textProperty().bind(Bindings.format("%.6f, %.6f",
            gpsPosition.latitudeProperty(), gpsPosition.longitudeProperty()));
        altitudeLabel.textProperty().bind(Bindings.format("%.1f m",
            gpsPosition.altitudeProperty()));
        speedLabel.textProperty().bind(Bindings.format("%.2f m/s",
            gpsPosition.velocityProperty()));
        directionLabel.textProperty().bind(Bindings.format("%.2f °",
            gpsPosition.directionProperty()));
        qualityLabel.textProperty().bind(Bindings.format("%d",
            gpsPosition.qualityProperty()));
        satellitesLabel.textProperty().bind(Bindings.format("%d",
            gpsPosition.satellitesProperty()));
        statusLabel.setText(MessageFormat.format(resources.getString("label.gps"),
            resources.getString("label.gps.not-fixed")));
        listView.managedProperty().bind(listView.visibleProperty());
        listView.visibleProperty().bind(showLog.selectedProperty());
        showLog.setSelected(false);
    }
    public void stop() {
        service.stop();
    }
    @FXML private void onExit(){
        Platform.exit();
    }
    @FXML private void onZoomIn() {
        if (mapView.getZoom() < 19) {
            mapView.setZoom(mapView.getZoom() + 1);
        }
    }
    @FXML private void onZoomOut() {
        if (mapView.getZoom() > 1) {
            mapView.setZoom(mapView.getZoom() - 1);
        }
    }
}
Listing 12-14

UIPresenter class

The GPSPosition and the GPSService objects are injected into the presenter, and the text properties of the different labels are bound to the JavaFX properties. Note that it is important to stop the service when the application is closed. This will close the serial port and release the GPIO controller.

The Application Class
Our main class will create a view for the scene and launch the application (Listing 12-15). It is important to set the scene dimensions based on the Raspberry Pi screen.
package org.modernclients.raspberrypi.gps;
import com.airhacks.afterburner.injection.Injector;
import javafx.application.Application;
import javafx.geometry.Rectangle2D;
import javafx.scene.Scene;
import javafx.stage.Screen;
import javafx.stage.Stage;
import org.modernclients.raspberrypi.gps.view.UIPresenter;
import org.modernclients.raspberrypi.gps.view.UIView;
public class MainApp extends Application {
    private UIPresenter controller;
    @Override
    public void start(Stage stage) throws Exception {
        Rectangle2D bounds = Screen.getPrimary().getBounds();
        UIView ui = new UIView();
        controller = (UIPresenter) ui.getPresenter();
        Scene scene = new Scene(ui.getView(),
            bounds.getWidth(), bounds.getHeight());
        stage.setTitle("Embedded Maps");
        stage.setScene(scene);
        stage.show();
    }
    @Override
    public void stop() throws Exception {
        controller.stop();
        Injector.forgetAll();
    }
    public static void main(String[] args) {
        launch(args);
    }
}
Listing 12-15

MainApp class

Finally, Listing 12-16 shows the module-info descriptor to generate the module org.modernclients.raspberrypi.gps, and Listing 12-17 shows the build.gradle file .
module org.modernclients.raspberrypi.gps {
    requires javafx.controls;
    requires javafx.fxml;
    requires pi4j.core;
    requires com.gluonhq.maps;
    requires afterburner.fx;
    requires java.annotation;
    requires java.logging;
    opens org.modernclients.raspberrypi.gps.model to afterburner.fx;
    opens org.modernclients.raspberrypi.gps.service to afterburner.fx;
    opens org.modernclients.raspberrypi.gps.view to afterburner.fx, javafx.fxml;
    exports org.modernclients.raspberrypi.gps;
}
Listing 12-16

module-info.java descriptor

plugins {
    id 'application'
    id 'org.openjfx.javafxplugin' version '0.0.8'
    id 'org.hidetake.ssh' version '2.10.1'
}
repositories {
    mavenCentral()
    maven {
        url 'http://nexus.gluonhq.com/nexus/content/repositories/releases/'
    }
}
dependencies {
    implementation 'com.pi4j:pi4j-core:1.2'
    implementation 'com.gluonhq:maps:2.0.0-ea+2'
    implementation 'com.gluonhq.attach:storage:4.0.2:desktop'
    implementation 'com.gluonhq.attach:util:4.0.2'
    implementation 'com.airhacks:afterburner.fx:1.7.0'
    implementation 'javax.annotation:javax.annotation-api:1.3.2'
}
javafx {
    modules = [ 'javafx.controls', 'javafx.fxml' ]
}
mainClassName = "$moduleName/org.modernclients.raspberrypi.gps.MainApp"
jar {
    manifest {
        attributes 'Main-Class': 'org.modernclients.raspberrypi.gps.MainApp'
    }
}
def workingDir = '/home/pi/ModernClients/ch12-RaspberryPi/'
def javaHome = '/opt/jdk-11.0.4+11'
def javafxHome = '/opt/armv6hf-sdk'
task libs(type: Copy) {
    dependsOn 'jar'
    into "${buildDir}/libs/"
    from configurations.runtimeClasspath
}
remotes {
    pi11 {
        host = 'raspberrypi.local'
        user = 'pi'
        password = 'pi'
    }
}
task runRemoteEmbedded {
    dependsOn 'libs'
    ssh.settings {
        knownHosts = allowAnyHosts
    }
    doLast {
        ssh.run {
            session(remotes.pi11) {
                execute "mkdir -p ${workingDir}/${project.name}/dist"
                fileTree("${buildDir}/libs")
                        .filter { it.isFile() && ! it.name.startsWith('javafx')}
                        .files
                        .each { put from:it, into: "${workingDir}/${project.name}/dist/${it.name}"}
                executeSudo "${javaHome}/bin/java -Dfile.encoding=UTF-8 " +
                        "--module-path=${javafxHome}/lib:${workingDir}/${project.name}/dist " +
                        "-Dembedded=monocle -Dglass.platform=Monocle " +
                        "-classpath '${workingDir}/${project.name}/dist/*' " +
                        "-m ${project.mainClassName}"
            }
        }
    }
}
Listing 12-17

build.gradle file

The complete project can be found here: https://github.com/modernclientjava/mcj-samples/tree/master/ch12-RaspberryPi/Sample6 .

Deploy and Test

Download the project, build it, and run it, to verify it works on your machine. Even if you don’t have GPS, it should display the UI with a map at a fixed location.

Then power up your Raspberry Pi, verify the display and the GPS are connected and located outdoors, and launch the gpsd service from an SSH terminal:
$ sudo service gpsd start
Now run from your machine
$ ./gradlew runRemoteEmbedded
And check that the app is deployed to the Raspberry Pi. If everything is working, you should be reading GPS sentences every second, getting updated latitude and longitude coordinates, and a map will be centered at your current location (Figure 12-19).
../images/468104_1_En_12_Chapter/468104_1_En_12_Fig19_HTML.jpg
Figure 12-19

DIY in-car navigation system running

You can run it directly as well from the SSH terminal (or from the Raspberry Pi with a keyboard):
$ cd /home/pi/ModernClients/ch12-RaspberryPi/embeddedGPS/dist
$ sudo java -p /opt/armv6hf-sdk/lib:. -Dembedded=monocle -Dglass.platform=Monocle -cp . -m org.modernclients.raspberrypi.gps/org.modernclients.raspberrypi.gps.MainApp

Next Challenge

If you were able to make it work, now the next challenge for you is to get your Raspberry Pi and display powered from a power bank, so you can run the app while moving, either walking or with a vehicle. It will be convenient to use tethering, creating a hotspot with your mobile device so the required maps can be downloaded from OpenStreetMap. You can add the SSID of your device to the wpa_supplicant.conf file as discussed at the beginning of this chapter.

Conclusions

In this chapter, you learned about how to configure a Raspberry Pi 3 Model B+ to work with Java 11 and JavaFX 11. With the help of basic samples, you saw how to run applications locally and how it was more convenient to use SSH and remote deployment while doing the development on your regular desktop machine.

Once the basics for running JavaFX applications were covered, you had the chance to learn about a more complex project involving a GPS sensor connected via GPIO pins, parsing NMEA readings, and using Gluon Scene Builder with the Afterburner framework to create the UI, which included Gluon Maps, to track your location.

While the Raspberry Pi is an embedded device, and it can’t be really compared with your regular machine, the actual Model B+ is a very capable device to run UI applications in places where a desktop machine wouldn’t fit.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.128.186.177