JavaFX17-现代-Java-客户端权威指南-六-

247 阅读24分钟

JavaFX17 现代 Java 客户端权威指南(六)

原文:The Definitive Guide to Modern Java Clients with JavaFX 17

协议:CC BY-NC-SA 4.0

十二、树莓派上的 JavaFX 17

由 José Pereda 撰写

在本章中,您将了解如何开始使用 Raspberry Pi 设备,以及运行 Java 和 JavaFX 17 应用程序所需的步骤,讨论进行本地或远程开发的方法以及如何进行远程部署。

您将看到不同的示例,从非常基本的 Java 和 JavaFX 应用程序开始,最后您将看到一个更复杂的项目,该项目试图在 GPS 设备的帮助下创建一个自制的车载导航系统。

树莓派简介

树莓派和 Arduino 是持续了 10 多年的创客运动的基石。但这些也是物联网(IoT)的基础,多年来,物联网不仅在业余爱好者中,而且在许多工业领域都得到了发展。它们甚至在 STEAM(科学、技术、工程、艺术和数学)计划中发挥了更大的作用,该计划直接针对我们孩子的教育。

事实上,Raspberry Pi 是作为一种小型、廉价的计算机诞生的,旨在供孩子们在学校的早期阶段学习编程。作为覆盆子的立国之本( www.raspberrypi.org )

我们的使命是将计算和数字制作的力量传递到世界各地的人们手中。

作为证明,Raspberry Pi 的常见发行版预装了 Scratch、Python 或 Java。截至 2021 年初,已售出超过 4000 万台,其中大部分对应于 2016 年发布的 Raspberry Pi 3 Model B,以及 2019 年发布的最新型号 Raspberry Pi 4 Model B。

无论你是一个业余爱好者,你从事专业的物联网项目,还是你有想要学习计算的孩子,都有很多理由可以让你考虑在一个树莓派上做一个非常小的投资。

本章将简要介绍如何开始使用它,以及如何在这个嵌入式设备上编程和运行 Java 和 JavaFX 应用程序。

树莓派入门

如何入门可以跟着 www.raspberrypi.org/documentation/

初始套件

在那里,您将根据您的预算找到所需的组件。以下是入门和完成本章示例的最低要求。

树莓派

如果你还没做过,买一个树莓派 4 型号 B 1 。您必须选择 1gb、2gb、4gb 或 8 GB 内存(取决于您的需求和预算)。

其主要规格如下:

  • SoC: Broadcom BCM2711,Cortex-A72 (ARMv8),64 位 SoC,1.5 GHz

  • GPU:500 MHz 的 Broadcom VideoCore VI

  • RAM: 1、2、4 或 8 GB LPDDR2 SDRAM

  • Wi-Fi 和蓝牙:2.4 GHz 和 5 GHz IEEE 802.11.b/g/n/ac 无线局域网,蓝牙 5.0

  • 网络:基于 USB 2.0 的千兆以太网

  • 图形:H.264 MPEG-4 解码(1080 p30);H.264 编码(1080 p30);OpenGL ES 3.1/3.2 图形

  • 通用输入/输出(GPIO):扩展的 40 引脚 GPIO 接头

  • 端口:2 个 USB 2.0 端口,2 个 USB 3.0 端口,2 个通过 micro-HDMI 的 HDMI;CSI 摄像机端口,用于连接 Pi 摄像机;DSI 显示端口,用于连接 Pi 触摸屏显示器;4 极立体声输出和复合视频端口

  • PoE:以太网供电支持

电源适配器

您可以购买完整的启动套件,也可以选择所需的附件,包括至少一个 5 V USB-C 电源适配器和一个 SD 卡。

sd 卡

跟随 www.raspberrypi.org/documentation/setup/

  • 选择 8gb 或 16 GB 的 SD 卡。我会选择 SanDisk Ultra micro sdhc 16 GB Class 10。

    有预装了 NOOBS 的 SD 卡,但也可以使用位于 www.raspberrypi.org/software/ 的 Raspberry Pi Imager 应用程序轻松下载和安装图像。

  • 键盘和鼠标是可选的。两者都需要 USB 连接。

班长

您可以使用任何带有 HDMI 连接的显示器或电视显示器,但有一个专用的 Raspberry Pi 触摸显示器,如下链接所述:

  • 它是一个 7 英寸的 LCD 显示器,通过 DSI 连接器连接到 Raspberry Pi:
 www.raspberrypi.org/documentation/hardware/display/README.md

  • 分辨率:全彩色显示器输出高达 800 × 480(与 HDMI 连接相比不是很好),具有能够检测十个手指的电容触摸感应功能。

  • 需要外接电源(使用另一个 micro-USB 电源比通过同一个 Pi 板连接更方便)。

  • 一个既能安装树莓皮又能展示的好盒子很方便。

  • 需要将显示器旋转 180 度(见本章下文)。

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

安装 SD

跟随 www.raspberrypi.org/documentation/installation/installing-img/README.md

我从 www.raspberrypi.org/software/operating-systems/ 用桌面软件镜像选择树莓派 OS 2 。当然,你可以选择 Lite(没有桌面和预装软件)或任何其他发行版。

总之:

  • 为您的操作系统下载并安装 Raspberry Pi Imager。打开后,你会看到如图 12-1 所示的图像

img/468104_2_En_12_Fig1_HTML.jpg

图 12-1

在 Mac 上运行 Raspberry Pi 成像仪

  • 选择图像,默认为 Raspberry Pi OS (32 位)带桌面。

    或者,对于 64 位,您可以从 https://downloads.raspberrypi.org/raspios_arm64/img/ 下载图像。下载 zip 文件并解压缩,以提取。iso 文件。然后选择菜单末端的Use custom选项。

  • 插入 SD 卡后,单击选择存储将文件写入其中。你可能需要一个 SD 读卡器。一个 USB SD 卡读卡器简单又非常方便。等到它完成,并提取 SD 卡。

树莓派配置

www.raspberrypi.org/documentation/configuration/。第一次启动时,如果你安装了桌面,它将启动 X11,并显示一个配置对话框。按照说明配置您的语言、键盘设置和 Wi-Fi。应用更改并重新启动。

一旦它重新启动,X11 启动,从顶部菜单,选择首选项➤树莓派配置。

或者,如果您从命令行或通过 SSH 运行,您可以通过运行以下命令获得如图 12-2 所示的基于终端的工具:

$ sudo raspi-config

img/468104_2_En_12_Fig2_HTML.jpg

图 12-2

运行 raspi-config

raspi 配置

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

  • 配置系统设置:

    • 更改用户密码:Raspbian 上的默认用户是pi,密码是raspberry。你可以在这里改变。为了方便,我将设置相同的密码:pi,这就方便了。当然,它与安全密码完全相反。

    • 主机名:它是该 Pi 在网络上的可见名称。如果需要,更改默认raspberrypi,例如,如果您有多个设备。

    • 无线局域网:如果在第一次 X11 会话中没有设置,请输入 SSID 和密码。它允许使用 Raspberry Pi 4 的内置无线连接来连接到无线网络,并允许您使用 SSH 从您通常的开发机器上远程工作。

    • 引导/自动登录:使用此选项将您的引导首选项更改为控制台或桌面。我们将选择控制台/命令行,但桌面也可以使用。为了方便起见,您也可以选择自动登录。

  • 本地化选项:

    • 如果您之前在第一次 X11 会话期间没有设置它,请从键盘布局、时区、区域设置和 Wi-Fi 国家代码中选取。这些菜单上的所有选项默认为英制或 GB,除非您更改它们。
  • 界面选项:

    • 相机:如果你有,你必须在这里启用它。

    • SSH:启用它;这是远程访问所必需的。

  • 性能选项:

    • GPU 内存:如果要运行 JavaFX,至少为 GPU 设置 256–512 MB。
  • 更新:推荐,但可能需要一段时间(仅当 Pi 连接到网络时)。

  • 完成后,重新启动。

请注意,您可以在/boot/config.txt文件中直接找到应用的设置。

再次登录后,您可以检查 Wi-Fi 设置:

$ sudo nano /etc/wpa_supplicant/wpa_supplicant.conf

您可以根据需要添加任意数量的 SSIDs。你可以用

$ sudo iwlist wlan0 scan

以找到任何给定位置的可用网络。

运行ifconfig查看您的 Pi 是否连接到网络。如图 12-3 所示,wlan0在一个给定的本地 IP 地址上被连接,数据包被接收和发送(RX,TX)。

img/468104_2_En_12_Fig3_HTML.jpg

图 12-3

运行 ifconfig

默认情况下,使用 DHCP。如果您需要一个静态 IP 地址,检查链接 www.raspberrypi.org/documentation/configuration/tcpip/README.md 并运行

$ sudo nano /etc/dhcpcd.conf

配置您的eth0wlan0静态 IP 地址。

如果您有 7 英寸的 Raspberry Pi 显示屏,您需要将其旋转 180 度。编辑config.txt文件:

$ sudo nano /boot/config.txt

在文件末尾添加以下内容:

lcd_rotate=2

然后,保存(Ctrl+O),退出(Ctrl+X)。

最后,请注意,在关闭 Raspberry Pi 之前,千万不要在它打开时从电源上拔下它,以防止损坏文件系统的风险。要获得关闭它的正确方法,请运行

$ sudo shutdown –h now

然后稍等片刻,断开电源。

通过 SSH 的远程连接

大多数时候,我们将通过 SSH 连接到 Raspberry Pi,以无标题方式运行(即 Pi 上没有专用的监视器和键盘),从我们的开发机器直接访问(包括复制/粘贴和两者之间的文件传输选项)。

SSH 内置于 Linux 发行版和 macOS 中。对于 Windows 和移动设备,可以使用第三方 SSH 客户端。在 Linux 和 macOS 上,您可以使用 SSH 从 Linux 电脑、Mac 或其他 Raspberry Pi 连接到您的 Raspberry Pi,而无需安装其他软件。在 Windows 上,最常用的客户端叫做 PuTTY,可以从 greenend.org.uk 下载。 www.raspberrypi.org/documentation/remote-access/ssh/windows10.md

通常您会通过以下方式登录

$ ssh pi@<IP>

您需要提供设备的 IP 地址。如果您已经连接到该 IP,您可以使用hostname –I来查找它,但如果不是这样,您可以尝试使用nmap或移动应用程序(如 Fing)在本地网络中查找该设备的 IP。

通常树莓派使用 DHCP,这意味着它没有固定的地址,重启后它可能会改变。这对于 SSH 连接来说并不方便。我们可以尝试设置一个固定的 IP,或者我们也可以尝试使用它的主机名来连接它,前提是它被广播到网络。这适用于 Raspberry Pi 操作系统,因为它使用多播 DNS 协议。

img/468104_2_En_12_Fig4_HTML.jpg

图 12-4

启动 SSH 会话

因为 macOS 和 Linux 都使用 Bonjour,所以都支持 mDNS。在 Windows 上,你可以从这里安装 Bonjour,比如 https://support.apple.com/kb/DL999

在这种情况下,您可以通过hostname.local登录

$ ssh pi@raspberrypi.local

输入密码后,您将可以访问树莓派(图 12-4 )。您将第一次看到安全性/真实性警告。键入 yes 继续。

最后,将您的开发机器的 SSH 公钥添加到设备上是很方便的,因此当运行 SSH 或 SCP 命令时,您不会一直被提示输入密码:

$ ssh-copy-id pi@<IP>

更多详情可以看看 www.raspberrypi.org/documentation/remote-access/ssh/passwordless.md

Java 11

树莓派 OS 自带 Java for ARM 安装。如果你运行java –version,它会打印出来

$ java -version
openjdk version "11.0.11" 2021-04-20
OpenJDK Runtime Environment (build 11.0.11+9-post-Debian-1deb10u1)
OpenJDK 64-Bit Server VM (build 11.0.11+9-post-Debian-1deb10u1, mixed mode)

但是,如果您在没有安装 Java 的发行版上运行(比如 Raspberry Pi OS Lite ),您可以很容易地用

$ sudo apt update
$ sudo apt install default-jdk

测试 Java 11

让我们测试 Java 11 和启动单文件源代码程序特性:

$ cd /home/pi/
$ mkdir ModernClients
$ cd ModernClients
$ nano Test.java

添加一个如清单 12-1 所示的 main 方法,或者从 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-1Sample0

保存,退出(Ctrl+O,Ctrl+X),然后运行:

$ java Test.java

应打印出图 12-5 的结果。

img/468104_2_En_12_Fig5_HTML.jpg

图 12-5

在树莓派上运行 Java 11

祝贺您在全新的 Raspberry Pi 上运行您的第一个 Java 11 应用程序!现在 Java 已经成功安装了,您可以进入下一步:安装 JavaFX。

安装 JavaFX 17

ARM 32 或 AArch64 的 JavaFX 17 builds 可以从 https://gluonhq.com/products/javafx 下载。这些 JavaFX 源代码与桌面平台(Windows、macOS、Linux)上使用的 Java FX 源代码完全相同,但是具有针对 ARM 和 32/64 位的特定驱动程序。注意 Linux 的常规 JavaFX 发行版不能工作,因为它是为 x86-64 构建的。

32 位

从 SSH 会话中,下载 SDK,将其移动到/opt,并解压缩:

$ wget https://gluonhq.com/download/javafx-17-ea-sdk-linux-arm32/ -O openjfx-17-ea+14_linux-arm32_bin-sdk.zip
$ sudo mv openjfx-17-ea+14_linux-arm32_bin-sdk.zip /opt
$ cd /opt
$ sudo unzip openjfx-17-ea+14_linux-arm32_bin-sdk.zip
$ sudo rm openjfx-17-ea+14_linux-arm32_bin-sdk.zip

如果您查看 lib 文件夹下的文件列表,您会发现不同 JavaFX 模块的 jar,以及 ARM 的本地库。

Note

虽然您会发现 media 和 web JavaFX 模块,但 ARM 尚不支持这些模块。摇摆也不是。

64 位

从 SSH 会话中,下载 SDK,将其移动到/opt,并解压缩:

$ wget https://gluonhq.com/download/javafx-17-ea-sdk-linux-aarch64-monocle/ -O openjfx-17-ea+14_monocle-linux-aarch64_bin-sdk.zip
$ sudo mv openjfx-17-ea+14_monocle-linux-aarch64_bin-sdk.zip /opt
$ cd /opt
$ sudo unzip openjfx-17-ea+14_monocle-linux-aarch64_bin-sdk.zip
$ sudo rm openjfx-17-ea+14_monocle-linux-aarch64_bin-sdk.zip

如果您查看 lib 文件夹下的文件列表,您会发现不同 JavaFX 模块的 jar,以及 ARM 的本地库。

Note

JavaFX SDK 64 bits 支持 ARM 上的媒体和 web JavaFX 模块。不支持 Swing。

直接呈现管理器(DRM)

DRM 是一个内核模块,它提供对直接渲染基础架构客户端的直接硬件访问。

Raspberry Pi 支持开源的 VC4/V3D DRM 驱动。与 Raspberry Pi 4 捆绑在一起的 GPU 是 Broadcom VideoCore VI,支持 OpenGL ES 3.2 并使用 V3D 驱动程序,而 Raspberry Pi 3 中的 Broadcom VideoCore IV 只能支持 OpenGL ES 2.0 并使用 VC4 驱动程序。

如果尚未启用,要访问硬件加速,您可以启用可选的覆盖编辑config.txt文件

$ sudo nano /boot/config.txt

在文件的末尾,您应该有

# Enable DRM VC4 V3D drive
dtoverlay=vc4-fkms-v3d

然后,保存您所做的任何修改(Ctrl+O),并退出(Ctrl+X)。如果需要,重新启动。

您应该检查设备/dev/dri/card0(或/dev/dri/card1)是否存在。

运行 JavaFX 应用程序

为了支持硬件加速渲染,JavaFX 依赖于许多低级驱动程序和库,这些驱动程序和库并不总是默认安装在所有嵌入式系统上。例如,Raspberry Pi OS Lite 发行版就是这种情况,您可以使用

$ sudo apt install libegl-mesa0 libegl1 libgbm1 libgles2 libpango-1.0.0 libpangoft2-1.0-0

JavaFX 对 DRM 的支持是 Gluon 的商业扩展。你可以通过设置环境变量ENABLE_GLUON_COMMERCIAL_EXTENSIONS来启用它,如果你的应用是非商业性的,或者如果你从 Gluon 获得了一个有效的许可(访问 https://gluonhq.com/contact-embedded/ )。

要在当前会话中启用它,请运行

$ export ENABLE_GLUON_COMMERCIAL_EXTENSIONS=true

或者将其添加到您的。bash 文件(为了方便起见,我们也导出PATH_TO_FX):

$ nano /home/pi/.bashrc
export ENABLE_GLUON_COMMERCIAL_EXTENSIONS=true
export PATH_TO_FX=/opt/javafx-sdk-17/lib

保存并退出。

要为任何远程会话启用它,将它添加到environment文件也很方便:

$ nano /etc/environment
ENABLE_GLUON_COMMERCIAL_EXTENSIONS=true

保存并退出。

样本 1

现在让我们试着从链接 https://github.com/modernclientjava/mcj-samples/tree/master/ch12-RaspberryPi/Sample1 运行 HelloFX 示例,它是基于 https://openjfx.io/openjfx-docs/ 示例的。

清单 12-2 包含了从应用程序 JavaFX 类扩展而来的 HelloFX Java 类的代码。

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-2Sample1

要现在运行它,从 SSH 会话中,让我们首先克隆带有示例的存储库:

$ cd /home/pi/Downloads
$ wget https://github.com/modernclientjava/mcj-samples/archive/master.zip
$ unzip master.zip
$ mv mcj-samples-master /home/pi/ModernClients

现在输入样本 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 -E java --module-path $PATH_TO_FX --add-modules=javafx.controls \
    -Dmonocle.platform=EGL -Dembedded=monocle -Dglass.platform=Monocle \
    -Dmonocle.egl.lib=$PATH_TO_FX/libgluon_drm-1.1.6.so \
    -cp dist/. org.modernclients.raspberrypi.HelloFX

该应用程序将会运行,但只会在连接的显示器上显示。您可以从 SSH 终端使用 Ctrl+C 退出应用程序。或者,您也可以尝试终止 Java 进程:

$ sudo killall -9 java

如果一切都按预期运行,您将得到图 12-6 中的结果,流程的输出将显示如下

[GluonDRM] use GPU at /dev/dri/card0 and display id -1

但是,如果该过程失败,并且您收到关于设备不具备 DRM 功能的警告,您可以再次尝试在前面的命令行中添加以下选项:

-Degl.displayid=/dev/dri/card1

img/468104_2_En_12_Fig6_HTML.png

图 12-6

在树莓派上运行 JavaFX 11

JavaFX 鼠标事件需要写权限才能访问硬件,这就是为什么我们需要使用sudo;否则,应用程序将会启动,但会在控制台上显示一个异常:

Udev: Failed to write to /sys/class/input/mice/uevent

检查您是否有权限访问输入设备:

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)
...

既然我们已经运行了示例,我们可以试着从 X11 会话中运行它,只要我们安装了带有桌面软件的 Raspberry Pi 操作系统。

从现在的树莓派,我们运行startx;然后我们打开一个终端,输入

$ cd /home/pi/ModernClients/ch12-RaspberryPi/Sample1
$ sudo java --module-path $PATH_TO_FX --add-modules=javafx.controls \
    -cp dist/. org.modernclients.raspberrypi.HelloFX

由于我们已经移除了单片眼镜选项,现在我们有了一个常规的窗口应用程序(图 12-7 )。我们可以用鼠标关闭它并停止 Java 进程。

img/468104_2_En_12_Fig7_HTML.png

图 12-7

在 X11 上运行 JavaFX

样本 2

现在让我们通过使用 Gradle 或 Maven 这样的构建工具,并利用 Maven Central 上提供的 ARM 32/AArch64 的 JavaFX 构件,来尝试简化命令行过程。

无论如何,你还是需要检索胶子 DRM 库。您可以在/opt/javafx-sdk-17/lib/libgluon_drm-1.1.6.so下下载包含它的整个 SDK,也可以从以下网址下载:

  • 32 位:

  • 64 位:

$ sudo wget http://download2.gluonhq.com/drm/lib-1.1.6/arm32/libgluon_drm.so -O /opt/javafx-sdk-17/lib/libgluon_drm-1.1.6.so

$ sudo wget http://download2.gluonhq.com/drm/lib-1.1.6/aarch64/libgluon_drm.so -O /opt/javafx-sdk-17/lib/libgluon_drm-1.1.6.so

格拉德尔

让我们运行样本 2:

$ cd /home/pi/ModernClients/ch12-RaspberryPi/Sample2/

清单 12-3 中显示的 build.gradle 文件包含任务run,该任务添加了运行进程所需的 JVM 参数。

plugins {
  id 'application'
}

repositories {
    mavenCentral()
}

def osArch = System.properties['os.arch']
def version = "17-ea+14"
def platform = osArch == "arm" ? "linux-arm32-monocle" :
              "linux-aarch64-monocle"

mainClassName = "org.modernclients.raspberrypi.HelloFX"

dependencies {
    implementation "org.openjfx:javafx-base:$version:$platform"
    implementation "org.openjfx:javafx-graphics:$version:$platform"
    implementation "org.openjfx:javafx-controls:$version:$platform"
}

compileJava {
    doFirst {
        options.compilerArgs = [
                '--module-path', classpath
                      .filter(j -> j.toString().contains(osArch)).asPath,
                '--add-modules', 'javafx.controls'
        ]
    }
}

run {
    doFirst{
        environment "ENABLE_GLUON_COMMERCIAL_EXTENSIONS", "true"
        jvmArgs = [
                '-Dmonocle.platform=EGL', '-Dembedded=monocle',
                '-Dglass.platform=Monocle',
                "-Dmonocle.egl.lib=
                   /opt/javafx-sdk-17/lib/libgluon_drm-1.1.6.so",
                '--module-path', classpath
                      .filter(j -> j.toString().contains(osArch)).asPath,
                '--add-modules', 'javafx.controls'
        ]
    }
}

Listing 12-3Sample2 build.gradle file

你可以直接跑

$ ./gradlew run

第一次它将下载 Gradle 7.0.1,并且它将创建一个 Gradle 守护进程,所以它可能需要一段时间才能开始。

按 Ctrl+C 退出应用程序。请注意,有时应用程序不会关闭,因为仍然有一些 Gradle 守护线程在运行。您可以通过找到 Java 进程的 ID 来阻止它们

$ ps –aux
$ sudo kill <pid of Java process>

或者直接与

$ sudo killall -9 java

还要注意的是,您可以通过编辑文件让 sudo 访问 Gradle 进程

$ nano gradlew

并在最后加上

exec sudo "$JAVACMD"...

保存并退出(Ctrl+O,Ctrl+X)。

专家

另一种选择是使用 Maven 工具和javafx-maven-plugin。您可以很容易地在您的 Pi 上安装 Maven

$ sudo apt-get install maven

清单 12-4 显示了运行示例所需的 pom.xml 文件。

<project xmlns:="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.modernclients.raspberrypi</groupId>
    <artifactId>hellofx</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.release>11</maven.compiler.release>
        <javafx.version>17-ea+14</javafx.version>
        <main.class>org.modernclients.raspberrypi.HelloFX</main.class>
        <runtime.jvm.options/>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-controls</artifactId>
            <version>${javafx.version}</version>
        </dependency>
    </dependencies>

    <profiles>
        <profile>
            <id>default</id>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
        </profile>
        <profile>
            <id>pi</id>
            <properties>
                <runtime.jvm.options>-Dmonocle.platform=EGL,
                  -Dembedded=monocle,-Dglass.platform=Monocle,
         -Dmonocle.egl.lib=/opt/javafx-sdk-17/lib/libgluon_drm-1.1.6.so
            </runtime.jvm.options>
            </properties>
        </profile>
    </profiles>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
            </plugin>
            <plugin>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-maven-plugin</artifactId>
                <version>0.0.8</version>
                <configuration>
                    <mainClass>${main.class}</mainClass>
                    <options>${runtime.jvm.options}</options>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

Listing 12-4Sample2 pom.xml file

在桌面上,你可以运行它

mvn javafx:run

在你的树莓皮上,你可以尽情奔跑

mvn -Ppi -Djavafx.monocle=true javafx:run

注意,现在所有的 JavaFX 依赖项都是从 Maven Central 下载的,我们必须使用-Djavafx.monocle=true来选择包含 Monocle 的依赖项。

远程运行 JavaFX 应用程序

虽然这些项目是在 Raspberry Pi 上本地编译和构建的,但与在您的机器上构建相比,它要慢得多,而且 IDE 的缺乏或 SSH 开发的不便促使人们寻找不同的方法:在您的常规机器上开发,然后在 Pi 上部署和运行。

另一方面,我们机器上的开发要快得多,但是我们仍然有部署问题:我们需要将应用程序的相关文件复制到 Raspberry Pi,然后才能在上面运行。

有几个选项可以复制所需的文件,比如经典的 FTP 甚至 SCP(通过 SSH 发送文件的命令)。这意味着你可以在电脑之间复制文件,从你的 Raspberry Pi 到你的台式机或笔记本电脑,反之亦然。

例如,假设我们有样本 3: modernclientjava/mcj-samples/tree/master/ch12-RaspberryPi/Sample3。我们在机器上用 Maven 编译并构建它,然后将结果类复制到 Raspberry Pi:

$ cd mcj-samples-master/ch12-RaspberryPi/Sample3
$ mvn clean compile
$ cd ..
$ scp -r Sample3 pi@raspberrypi.local:/home/pi/ModernClients/ch12-RaspberryPi/Sample3
(add password)

现在,我们可以从 SSH 终端运行:

$ cd /home/pi/ModernClients/ch12-RaspberryPi/Sample3/target
$ sudo -E java --module-path $PATH_TO_FX:classes -Dmonocle.platform=EGL \
     -Dembedded=monocle -Dmonocle.egl.lib=
          /opt/javafx-sdk-17/lib/libgluon_drm-1.1.6.so \
    -Dglass.platform=Monocle \
    -m hellofx/org.modernclients.raspberrypi.MainApp

虽然这是可行的,但这是一个乏味且容易出错的手动过程,如果我们能够将这一步骤集成到我们的 IDE 中,或者我们能够为我们的构建工具提供一个插件,那就更好了。

让我们检查一些选项。

Java 远程平台

NetBeans 不久前提出了远程平台的概念。您可以在另一台机器上定义 JVM 的设置,并使用 Ant 任务在那台机器上部署和运行 SSH。

这对于树莓派来说非常方便。

要安装 Apache NetBeans 12.4,您可以访问链接 https://netbeans.apache.org/download/nb124/nb124.html 并选择适合您平台的安装程序。

安装完成后,请转到工具➤ Java 平台。单击添加平台…并选择远程 Java 标准版。

提供关于平台的一些细节:远程平台的名称,(例如Pi 17);主机(可以是raspberrypi.local);用户,pi;密码;和远程 JRE 路径,/usr/lib/jvm/java-11-openjdk-arm64。见图 12-8 。

img/468104_2_En_12_Fig8_HTML.jpg

图 12-8

远程平台配置

创建远程平台后,确保将sudo添加到 exec 前缀,如图 12-9 所示。

最后,在 Raspberry Pi 可用的情况下,单击 Test Platform,看到测试成功了。否则,请确保所有字段都设置正确。

让我们用一个例子来试试远程平台。按照这里的说明, https://openjfx.io/openjfx-docs/#IDE-NetBeans ,在没有构建工具的情况下创建一个新的 Java 应用程序,或者从链接 https://github.com/modernclientjava/mcj-samples/tree/master/ch12-RaspberryPi/Sample4 下载 Sample4。

首先,确保应用在你的机器上运行良好。

现在在 NetBeans 上,编辑Properties,选择RunRuntime Platform,挑选Pi 17。提供一个配置名,如Pi17。确保提供 JavaFX SDK 的路径,并在 VM 选项中包含 Monocle 选项,如图 12-10 所示

--module-path /opt/javafx-sdk-17/lib --add-modules=javafx.controls -Dembedded=monocle -Dglass.platform=Monocle

并关闭对话框。

img/468104_2_En_12_Fig10_HTML.png

图 12-10

设置项目属性以在远程平台上运行

img/468104_2_En_12_Fig9_HTML.png

图 12-9

向远程平台添加 exec 前缀

当在桌面或 Raspberry Pi 上运行相同的应用程序时,可以方便地根据它运行的平台来调整它的窗口大小,如清单 12-5 所示。

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-5Configure window size based on platform

现在从 Pi17 配置再次运行。您将在 NetBeans 输出窗口中看到连接详细信息:

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';
'/usr/lib/jvm/java-11-openjdk-arm64/bin/java'  -Dfile.encoding=UTF-8
 --module-path=/opt/javafx-sdk-17/lib -Dmonocle.platform=EGL
-Dembedded=monocle -Dmonocle.egl.lib=
      /opt/javafx-sdk-17/lib/libgluon_drm-1.1.6.so
-Dglass.platform=Monocle --add-modules=javafx.controls  -jar /home/pi/NetBeansProjects//Sample4/dist/HelloFX11.jar

您将在 Pi 的显示屏上看到您的应用程序运行良好,同时您可以在 NetBeans 输出窗口中看到该过程的输出。您甚至可以调试应用程序。

但是,这种方法有几个问题:它只能在 NetBeans 上工作,对于 Maven 或 Gradle 项目无效。

Gradle SSH Plugin

另一个选项是来自 https://gradle-ssh-plugin.github.io 的 SSH Gradle 插件。它将在终端或任何支持 Gradle 的 IDE 上运行 Gradle 项目。

现在让我们从 IntelliJ(或者您选择的任何 IDE)运行这个示例, https://github.com/modernclientjava/mcj-samples/tree/master/ch12-RaspberryPi/Sample5

编辑build.gradle文件,并验证所需的配置:工作目录、Java home、JavaFX 路径和您的远程配置(主机、用户和密码),如清单 12-6 所示。

plugins {
    id 'application'
    id 'org.openjfx.javafxplugin' version '0.0.10'
    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 = '/usr'
def javafxHome = '/opt/javafx-sdk-17/lib'
task libs(type: Copy) {
    dependsOn 'jar'
    into "${buildDir}/libs/"
    from configurations.compileClasspath
}
remotes {
    pi17 {
        host = 'raspberrypi.local'
        user = 'pi'
        password = 'pi'
    }
}
task runRemoteEmbedded {
    dependsOn 'libs'
    ssh.settings {
        knownHosts = allowAnyHosts
    }
    doLast {
        ssh.run {
            session(remotes.pi17) {
                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 "-E ${javaHome}/bin/java -Dfile.encoding=UTF-8 " +
                        "--module-path=${javafxHome}/lib:
                        ${workingDir}/${project.name}/dist " +
                        "-Dmonocle.platform=EGL -Dembedded=monocle
                         -Dglass.platform=Monocle " +
                        "-Dmonocle.egl.lib=
                           ${javafxHome}/libgluon_drm-1.1.6.so " +
                          "-classpath '${workingDir}/${project.name}/dist/*' " +
                        "-m ${project.mainClassName}"
            }
        }
    }
}

Listing 12-6Gradle build file for Sample5

Note

为了方便,任务设置allowAnyHosts,主机密钥检查关闭。它将打印一条警告消息,指出该过程容易受到中间人攻击,不建议将其用于生产。

有了这个插件,在终端按 Ctrl+C 只会杀死 Gradle 进程,而不会杀死应用程序。要解决这个问题,一定要在用户界面上添加一个“退出”按钮。

从 IDE Gradle 的窗口运行runRemoteEmbedded任务,如图 12-11 ,或者从终端运行:

$ ./gradlew runRemoteEmbedded

应用程序将被构建、部署到 Pi 并在其上执行,您将在您的终端中获得流程的输出,如图 12-11 所示。

img/468104_2_En_12_Fig11_HTML.png

图 12-11

执行 runRemoteEmbedded 任务

创建 JavaFX 本机映像

您可以创建 JavaFX 应用程序的本机映像,并在 Raspberry Pi 上运行它,唯一的要求是 AArch64 (64 位)架构是当前唯一受支持的架构。

鉴于设备的硬件限制和本机映像进程的高 CPU/内存要求,创建本机映像的推荐方法是从桌面 Linux 机器(x86-64)进行交叉编译。

这台机器应该有从 https://github.com/gluonhq/graal/releases/latest 下载并安装的 GraalVM for Linux,包括

$ export GRAALVM_HOME=/path/to/graalvm-svm-linux-gluon-21.2.0-dev

然后,对于交叉编译,您还需要

$ sudo apt-get install g++-aarch64-linux-gnu

清单 12-7 显示了定义了pi概要文件的样本 6 的 pom 文件。它包括一些可能会根据您的设置而改变的属性。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.modernclients.raspberrypi</groupId>
    <artifactId>hellofx</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>HelloFX</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.release>11</maven.compiler.release>
        <javafx.version>17-ea+14</javafx.version>
        <javafx.maven.plugin.version>0.0.6</javafx.maven.plugin.version>
        <gluonfx.maven.plugin.version>1.0.3</gluonfx.maven.plugin.version>
        <runtime.jvm.options/>
        <runtime.options/>
        <remote.host.name/>
        <remote.dir/>
        <main.class>org.modernclients.raspberrypi.HelloFX</main.class>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-controls</artifactId>
            <version>${javafx.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
            </plugin>

            <plugin>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-maven-plugin</artifactId>
                <version>${javafx.maven.plugin.version}</version>
                <configuration>
                    <mainClass>${main.class}</mainClass>
                    <options>${runtime.jvm.options}</options>
                </configuration>
            </plugin>

            <plugin>
                <groupId>com.gluonhq</groupId>
                <artifactId>gluonfx-maven-plugin</artifactId>
                <version>${gluonfx.maven.plugin.version}</version>
                <configuration>
                    <target>${gluonfx.target}</target>
                    <mainClass>${main.class}</mainClass>
                    <runtimeArgs>${runtime.options}</runtimeArgs>
                    <remoteHostName>${remote.host.name}</remoteHostName>
                    <remoteDir>${remote.dir}</remoteDir>
                </configuration>
            </plugin>
        </plugins>

    </build>

    <profiles>
        <profile>
            <id>pi</id>
            <properties>
                <gluonfx.target>linux-aarch64</gluonfx.target>
                <remote.host.name>pi@raspberrypi.local</remote.host.name>
                <remote.dir>/home/pi/ModernClients/
                     ch12-RaspberryPi/Sample6</remote.dir>
                <runtime.options>-Duse.fullscreen=true,
                     -Dmonocle.platform=EGL,-Dembedded=monocle,
                     -Dglass.platform=Monocle</runtime.options>
                <runtime.jvm.options>-Dmonocle.egl.lib=
                     /opt/javafx-sdk-17/lib/libgluon_drm-1.1.6.so,
                     ${runtime.options}</runtime.jvm.options>
            </properties>
        </profile>
    </profiles>

</project>

Listing 12-7Maven pom file for Sample6

在您的 Linux 机器上获得 Sample6:

$ wget https://github.com/modernclientjava/
mcj-samples/archive/refs/heads/master.zip -O ~/Downloads/ModernClients.zip
$ unzip ~/Downloads/ModernClients.zip
$ cd ~/Downloads/mcj-samples-master/ch12-RaspberryPi/Sample6

现在可以使用以下代码构建本机映像

$ mvn -Ppi gluonfx:build

这将运行编译阶段,并将编译后的对象链接到可执行文件中。几分钟后,一旦该过程完成,您就可以将二进制文件部署到您的 Pi,前提是您已经在 pom 中正确定义了remoteHostNameremoteDir:

$ mvn -Ppi gluonfx:install

最后,您可以通过 SSH 从您的机器上运行,使用

$ mvn -Ppi gluonfx:nativerun

按 Ctrl+C 完成应用程序。

或者,您也可以从命令行直接在 Pi 上运行本机映像

$ cd /home/pi/ModernClients/ch12-RaspberryPi/Sample6
$ sudo -E ./HelloFX -Dmonocle.platform=EGL \
-Dembedded=monocle -Dglass.platform=Monocle

使用依赖项

到目前为止,我们已经看到了非常简单的用例,它们有助于我们开始并正确设置一切。

现在我们来看一个更复杂的例子。

DIY 车载导航系统

以下项目是一个自制车载导航系统的概念验证。为此,我们将在树莓派上安装 GPS。JavaFX 应用程序将显示一张地图,GPS 读数将用于在我们当前位置的地图中心。

材料清单

树莓派 4 型号 B

7″显示屏 800×480;笼子是可选的,但建议使用。

树莓派和显示器的 5 V 电源适配器。电源组是可选的,但建议用于现场测试。

GPS:通用异步收发器(UART)系列 GPS Neo-7M (micro-USB 可选)(图 12-12 ),比如这个: http://wiki.keyestudio.com/index.php/KS0319_keyestudio_GPS_Module

用于 GPIO 连接的四根母-母跳线。

微型 USB: USB 适配器(如果 GPS 分线架安装微型 USB)是可选的。

GPS 天线是可选的(但使用时,必须移除电容器 C2)。

img/468104_2_En_12_Fig12_HTML.jpg

图 12-12

UART 串行 GPS Neo-7M。图片来自 http://wiki.keyestudio.com/File:KS0319.png

GPIO 设置

我们将使用通用输入/输出(GPIO)引脚从 GPS 获取串行读数。

Raspberry Pi 串行端口由两个信号组成,一个发送信号(TxD)和一个接收信号(RxD),可在 4 Model B 上的引脚 8 和 10 处获得(相当于图 12-13 中的接线 Pi 编号 15 和 16,按此顺序)。

img/468104_2_En_12_Fig13_HTML.jpg

图 12-13

树莓派 4 型号 B GPIO 引脚排列。图片来自 https://pi4j.com/assets/documentation/headerpins_in_header.png

默认情况下,Raspberry Pi 上的串行端口被配置为控制台端口,用于与 Linux OS shell 通信。为了从软件程序访问串行端口,我们必须对其进行配置。打开一个 SSH 会话,运行

$ sudo raspi-config

选择接口选项,现在选择串行端口。

现在,您必须选择No来禁用登录 shell 对串行的访问,然后选择Yes来启用硬件串行端口(或通用异步收发器的 UART)。接受并重启你的树莓派。

GPIO 连接

GPS 模块需要四个连接,可使用四根跨接线母-母连接;参见图 12-14 ,从 GPIO 引脚的右到左:

  • VCC 引脚连接到引脚 2(电源 5 V),红色跳线

  • GND 引脚连接到引脚 6(接地),黄色跳线

  • RXD 引脚连接到引脚 8 (TxD UART,WiringPi 15),绿色跳线

  • TXD 引脚连接到引脚 10 (RxD UART,WiringPi 16),蓝色跳线

img/468104_2_En_12_Fig14_HTML.png

图 12-14

GPS 和 Raspberry Pi 4 型 GPIO 连接

请注意,可以使用分线板和引脚带状电缆,将 GPIO 引脚延伸到试验板,这样可以更容易地连接到 GPS。

所需的 GPS 软件

我们需要从终端安装以下软件

$ sudo apt-get install gpsd gpsd-clients

其中gpsd是 GPS 接收器的接口守护程序。完成后,如果已经连接了 GPS,您可以从编号最小的串行端口开始读取,方法是

$ gpsd /dev/ttyS0

或者,如果您已将 USB 连接到

gpsd /dev/ttyUSB0

启动gpsd的最佳选择是这项服务:

$ sudo service gpsd start

服务启动后,您可以使用以下命令验证其状态

$ sudo systemctl status gpsd.socket

这将显示如图 12-15 所示的内容。

img/468104_2_En_12_Fig15_HTML.png

图 12-15

gpsd 服务已启动

如果需要,您可以通过编辑文件来修改默认设置

$ sudo nano /etc/default/gpsd

一旦一切正常运行,您就可以使用

$ cgps /dev/ttyS0

或者用

$ gpsmon /dev/ttyS0

如果你在室内,全球定位系统很可能无法连接到任何卫星,你不会收到任何价值。但是你仍然会得到一些读数。

如果你把你的 Raspberry Pi 带到户外,只要 Wi-Fi 连接保持,你仍然可以通过 SSH 连接到你的机器,并可视化这些读数,得到如图 12-16 所示的东西。

img/468104_2_En_12_Fig16_HTML.png

图 12-16

gpsd 服务处于活动状态并正在运行

NMEA 读物

NMEA 是国家海洋电子协会的首字母缩写,NMEA 0183 是所有 GPS 制造商支持的标准数据格式,使用 ASCII 串行通信协议。有不同的消息类型或句子,它们都以标题$GP和句子代码开始,如GLL代表地理位置、纬度、经度,以*和校验和结尾。一条可能的消息看起来像这样

$GPGLL,5139.69658,N,00947.18207,W,200557.00,A,A*72

要了解所有可能的句子以及如何解析它们,可以看这个链接: http://aprs.gids.nl/nmea/

清单 12-8 显示了我们将在应用程序中使用的模型类,用于跟踪来自 GPS 的一些变量,如纬度、经度、高度或卫星数量,清单 12-9 显示了最重要的 NMEA 消息(如 GPRMC 或 GPGGA)的可能解析器。

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-9NMEAParser class

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-8GPSPosition class

GPIO 和 Java
Pi4J

Pi4J 是一个 Java 库,可用于访问 Raspberry Pi 的 GPIO 引脚。正如你可以在 http://pi4j.com/ 读到的

这个项目旨在为 Java 程序员提供一个友好的面向对象的 I/O API 和实现库,以访问 Raspberry Pi 平台的全部 I/O 功能。这个项目抽象了低级本机集成和中断监控,使 Java 程序员能够专注于实现他们的应用程序业务逻辑。

我们将使用它最新的稳定版本 1.4,所以我们只需要在构建中包含依赖关系:

dependencies {
    implementation 'com.pi4j:pi4j-core:1.4'
}

使用 Pi4J,创建一个Serial对象就像

Serial serial = SerialFactory.createInstance();

然后,我们可以向它添加一个侦听器,这样我们就可以对任何传入的串行事件做出反应,并且我们可以根据通常的设置来配置串行。

注意,这个库只能在 Raspberry Pi 上运行,但是它可以在您的机器上使用和编译。

威灵皮

在使用 Pi4J 之前,必须在 Raspberry Pi: WiringPi 上提供一个本机依赖项。

虽然 Raspberry Pi 3 型号内置了它,但在型号 4 B 上,您需要执行以下步骤来安装最新(非官方)版本:

sudo apt-get remove wiringpi -y
sudo apt-get --yes install git-core gcc make
cd ~/Downloads
git clone https://github.com/WiringPi/WiringPi --branch master
   --single-branch wiringpi
sudo ~/buildings/wiringpi/build

GPS 服务

清单 12-10 显示了打开串口并开始监听串行事件的服务类,一个接一个地提取从gpsd进程接收到的所有句子。

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("\n", "")
                        .replaceAll("\r", "");
                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-10GPSService class

当从一个串行端口读取时,我们必须意识到我们有一个连续的字节流,所以我们必须把它们正确地转换成字符串并取出每个句子。这就是processReading方法在单个StringBuilder的帮助下所做的事情。

另外,注意这个线程是在后台运行的,所以每当有一个新句子时,我们将在 JavaFX 应用程序线程上使用Platform::runLater和 JavaFX 属性一起使用它。

对于每个句子,我们将调用 NMEA 解析器并用新值更新 GPSPosition 对象。

用户界面

现在让我们定义 JavaFX 接口:我们将显示一张地图,该地图将以从 GPS 读数中检索到的纬度和经度坐标为中心。

胶子图

Gluon Maps ( https://gluonhq.com/labs/maps/ )是一个开源的 JavaFX 11+库,它提供了一个地图查看器组件,从 OpenStreetMap 呈现基于图块的地图。此处项目可用: https://github.com/gluonhq/maps

我们可以在视图的中心添加一个MapView容器,并使用一个MapLayer来呈现我们的位置。在内置 GPS 传感器的移动设备上,我们可以使用胶子定位服务,但在 Raspberry Pi(或任何连接了 GPS 传感器的台式机)上,我们可以使用前面文本中列出的GPSService

要添加地图,我们需要以下依赖项:

repositories {
    mavenCentral()
    maven {
       url 'https://nexus.gluonhq.com/nexus/content/repositories/releases/'
    }
}
dependencies {
    implementation 'com.gluonhq:maps:2.0.0-ea+4'
    implementation 'com.gluonhq.attach:storage:4.0.11:desktop'
    implementation 'com.gluonhq.attach:util:4.0.11'
}

为了方便起见,我们将定义一个PoiLayer,它可以基于纬度和经度将 JavaFX 节点放置在基本地图的顶部,这将是我们感兴趣的点(清单 12-11 )。

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-11PoiLayer class

加力燃烧室

Afterburner 是一个方便的极简 MVP 框架,基于 Adam Bien 的配置约定和依赖注入,可以在这个链接找到: https://github.com/AdamBien/afterburner.fx 。为了使用它,我们需要

repositories {
    mavenCentral()
}
dependencies {
    implementation 'com.airhacks:afterburner.fx:1.7.0'
    implementation 'javax.annotation:javax.annotation-api:1.3.2'
}

场景构建器

最后,我们将使用 https://gluonhq.com/products/scene-builder/ 的场景构建器 16.0 在我们的机器上用 FXML 设计 UI。

将胶子图导入到 Scene Builder 自定义控件库中很方便(图 12-17 )。

img/468104_2_En_12_Fig17_HTML.png

图 12-17

将胶子贴图导入场景构建器

然后我们可以创建一个新的 FXML 文件,带有一个顶部的BorderPane容器,并拖放所需的组件:一个工具栏在顶部,一个MapView在中间,一个VBox在右边带有标签以显示当前的 GPSPosition 值,一个ListView在底部显示 NMEA 句子(图 12-18 )。

img/468104_2_En_12_Fig18_HTML.png

图 12-18

在场景构建器中设计用户界面

请注意,使用加力燃烧室,我们将创建以下文件:

Java 类:

  • 从 FXMLView 扩展而来的org.modernclients.raspberrypi.gps.view.UIView(清单 12-12 ),FXML view 是一个方便的容器,按照惯例负责加载 FXML、CSS 或属性文件

  • org.modernclients.raspberrypi.gps.view.UIPresenter(列表 12-16 )

资源文件:

  • org . modern clients . raspberrypi . GPS . view . ui . fxml(列表 12-13 )

  • org . modern clients . raspberrypi . GPS . view . ui . CSS(列表 12-14 )

  • org . modern clients . raspberrypi . GPS . view . ui . properties(列表 12-15 )

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-15ui.properties 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-14ui.css file

<?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-13ui.fxml file

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(bundleName);
    }
}

Listing 12-12UIView class

一旦我们有了所有这些文件,现在是时候添加演示者了(清单 12-16 )。

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-16UIPresenter class

GPSPositionGPSService对象被注入到 presenter 中,不同标签的文本属性被绑定到 JavaFX 属性。请注意,在应用程序关闭时停止服务是很重要的。这将关闭串行端口并释放 GPIO 控制器。

应用程序类

我们的主类将为场景创建一个视图并启动应用程序(清单 12-17 )。基于 Raspberry Pi 屏幕设置场景尺寸很重要。

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-17MainApp class

最后,清单 12-18 显示了生成模块org.modernclients.raspberrypi.gps的模块信息描述符,清单 12-19 显示了build.gradle文件。

plugins {
    id 'application'
    id 'org.openjfx.javafxplugin' version '0.0.10'
    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.4'
    implementation 'com.gluonhq:maps:2.0.0-ea+4'
    implementation 'com.gluonhq.attach:storage:4.0.11:desktop'
    implementation 'com.gluonhq.attach:util:4.0.11'
    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/Sample7'
def javaHome = '/usr'
def javafxHome = '/opt/javafx-sdk-17/lib'
task libs(type: Copy) {
    dependsOn 'jar'
    into "${buildDir}/libs/"
    from configurations.runtimeClasspath
}
remotes {
    pi17 {
        host = 'raspberrypi.local'
        user = 'pi'
        password = 'pi'
    }
}
task runRemoteEmbedded {
    dependsOn 'libs'
    ssh.settings {
        knownHosts = allowAnyHosts
    }
    doLast {
        ssh.run {
            session(remotes.pi17) {
                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 "-E ${javaHome}/bin/java -Dfile.encoding=UTF-8 " +
                        "--module-path=${javafxHome}/lib:
                        ${workingDir}/${project.name}/dist " +
                        "-Dmonocle.platform=EGL -Dembedded=monocle
                         -Dglass.platform=Monocle " +
                        "-Dmonocle.egl.lib=
                           ${javafxHome}/libgluon_drm-1.1.6.so " +
                          "-classpath '${workingDir}/${project.name}/dist/*' " +
 "-m ${project.mainClassName}"
            }
        }
    }
}

Listing 12-19build.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-18module-info.java descriptor

完整的项目可以在这里找到:

https://github.com/modernclientjava/mcj-samples/tree/master/ch12-RaspberryPi/Sample7

部署和测试

下载项目,构建它,并运行它,以验证它在您的机器上工作。即使你没有 GPS,它也应该在固定位置显示带有地图的 UI。

然后启动您的 Raspberry Pi,验证显示器和 GPS 已连接并位于室外,并从 SSH 终端启动gpsd服务:

$ sudo service gpsd start

现在从你的机器上运行

$ ./gradlew runRemoteEmbedded

并检查应用程序是否已部署到 Raspberry Pi。如果一切正常,你应该每秒都在读取 GPS 语句并获得更新的经纬度坐标,一张地图将会以你当前的位置为中心(图 12-19 )。

img/468104_2_En_12_Fig19_HTML.png

图 12-19

DIY 车载导航系统运行

您也可以从 SSH 终端直接运行它(或者使用键盘从 Raspberry Pi 运行):

$ cd /home/pi/ModernClients/ch12-RaspberryPi/embeddedGPS/dist
$ sudo -E java -p /opt/javafx-sdk-17/lib:. -Dmonocle.platform=EGL
  -Dembedded=monocle -Dglass.platform=Monocle
  -Dmonocle.egl.lib/opt/javafx-sdk-17/lib/libgluon_drm-1.1.6.so
  -cp . -m org.modernclients.raspberrypi.gps/org.modernclients.raspberrypi.gps.MainApp

下次挑战

如果你能够让它工作,现在你的下一个挑战是让你的树莓派和显示器由一个电源供电,这样你就可以在移动时运行应用程序,无论是走路还是开车。使用网络共享会很方便,用你的移动设备创建一个热点,这样就可以从 OpenStreetMap 下载所需的地图。如本章开头所述,您可以将设备的 SSID 添加到wpa_supplicant.conf文件中。

结论

在本章中,您了解了如何配置 Raspberry Pi 4 Model B 来与 Java 11+和 JavaFX 17 一起工作。在基本示例的帮助下,您看到了如何在本地运行应用程序,以及在常规桌面计算机上进行开发时如何更方便地使用 SSH 和远程部署。

一旦讲述了运行 JavaFX 应用程序的基础知识,您就有机会了解一个更复杂的项目,包括通过 GPIO 引脚连接的 GPS 传感器、解析 NMEA 读数,以及使用带有 Afterburner 框架的 Gluon Scene Builder 创建 UI,其中包括 Gluon 地图,以跟踪您的位置。

虽然 Raspberry Pi 是一款嵌入式设备,不能与普通机器相比,但实际的 Pi 4 Model B 是一款非常有能力的设备,可以在台式机不适合的地方运行 UI 应用程序。

Footnotes 1

在撰写本文时,4 B 是最新的型号。

  2

在撰写本文时,Raspberry Pi 操作系统是可用的最新发行版。