nikita.kopot 27 октября 2020

☕ Начало работы с JavaFX на Raspberry Pi

Raspberry Pi идеально подходит для любителей поэкспериментировать с электронными компонентами. Если объединить его с инструментами Java-разработчика, вы откроете для себя новый мир.
☕ Начало работы с JavaFX на Raspberry Pi

Недорогой одноплатный компьютер Raspberry Pi отлично работает с Java, отчего hardware-разработка становится не сложнее классической разработки ПО.

***

Из этой статьи вы узнаете, как разработать приложение на JavaFX в стиле дашборд, используя библиотеку TilesFX.

На рис. 1 изображен пользовательский интерфейс (далее UI).

<b>Рис. 1</b> UI приложения
Рис. 1 UI приложения

Также вы можете ознакомиться с видео, на котором показана работа приложения на Raspberry Pi 3B+ и функциональность интерфейса с тачскрином.

Приведенный код и рассмотренные методы применимы только к микрокомпьютерам Raspberry Pi на чипах ARM v7 и ARM v8. В разделе спецификаций плат Raspberry Pi на Википедии вы найдете типы поставляющихся с этими процессорами плат:

Модель A+, версия 3;

Модель B, версии 2, 3 и 4;

Вычислительный модуль (Compute Module), версия 3.

Прочие используемые в проекте электронные компоненты вы найдете в большинстве стартовых наборов Arduino/Pi. Если вы хотите использовать другие элементы, можете начать с компонентов из проекта, и подстроить их под свои нужны. Мой комплект выглядит следующим образом:

Raspberry Pi 3 Модель B+;

SD карта 32 GB (или более) на ОС Raspberry Pi (бывш. Raspbian);

Монитор, клавиатура и мышь;

Светодиод и резистор (для большинства подойдет 330 Ом);

Кнопочный переключатель;

Датчик расстояния HC-SR04;

Беспаечная макетная плата и провода.

☕ Подтянуть свои знания по Java вы можете на нашем телеграм-канале «Библиотека Java для собеса»

Подготовка платы Raspberry Pi

Если вы работаете Raspberry Pi впервые, подготовьте карту SD с операционной системой. В проекте используется Full ОС Raspbian (полная версия). Загрузите инструмент Imager. Для своей работы я взял версию Raspberry Pi Imager 1.2 от марта 2020 г. (рис. 2 и рис. 3). Убедитесь, что устанавливаете полную версию.

<b>Рис. 2 </b>Страница скачивания инструмента Imager
Рис. 2 Страница скачивания инструмента Imager
<b>Рис.3</b> Выберите полную версию (Raspbian Full)
Рис.3 Выберите полную версию (Raspbian Full)

Как только SD карта будет готова, вставьте ее в плату Raspberry Pi, запустите операционную систему и выполните действия по настройке и подключению к вашей сети Wi-Fi.

🧩☕ Интересные задачи по Java для практики можно найти на нашем телеграм-канале «Библиотека задач по Java»

Установка JDK (Java Development Kit) с помощью JavaFX

В примечаниях к релизу Raspbian указано, что используемая мной версия ОС включает OpenJDK 11.

        2019-06-20:
* Based on Debian Buster
* Oracle Java 7 and 8 replaced with OpenJDK 11

    

Java-версия:

        $ java -version
openjdk version "11.0.3" 2019-04-16 
OpenJDK Runtime Environment (build 11.0.3+7-post-Raspbian-5) 
OpenJDK Server VM (build 11.0.3+7-post-Raspbian-5, mixed mode)

    

Теперь плата готова к запуску любых приложений на основе Java 11. Однако JavaFX больше не является частью JDK (начиная с Java 11), и запустить JavaFX-программу на Raspberry Pi «из коробки» не получится.

К счастью, BellSoft разработала Liberica JDK. Предназначенная для Raspberry Pi версия включает JavaFX, поэтому вы сможете запускать упакованное приложение JavaFX с помощью команды java -jar yourapp.jar. Используйте ссылку для загрузки от BellSoft, чтобы установить альтернативную JDK:

        $ cd /home/pi 
$ wget https://download.bell-sw.com/java/13/bellsoft-jdk13-linux-arm32-vfp-hflt.deb 
$ sudo apt-get install ./bellsoft-jdk13-linux-arm32-vfp-hflt.deb 
$ sudo update-alternatives --config javac 
$ sudo update-alternatives --config java

    

Когда инсталляция будет завершена, проверьте версию снова. Она должна выглядеть следующим образом:

        $ java --version 
openjdk version "13-BellSoft" 2019-09-17 
OpenJDK Runtime Environment (build 13-BellSoft+33) 
OpenJDK Server VM (build 13-BellSoft+33, mixed mode)

    

На моей тестовой плате хранятся различные версии Liberica JDK. Переключаться между ними несложно с помощью команды update-alternatives. (см. Рис.4)

<b>Рис. 4 </b>Переключение между версиями Liberica JDK
Рис. 4 Переключение между версиями Liberica JDK

В папке исходного кода Chapter_04_Java/scripts на GitHub расположены установочные скрипты различных версий Liberica JDK. Они содержат ссылки для скачивания. (см. Рис. 5)

<b>Рис. 5</b> Установочные скрипты различных версий Liberica JDK
Рис. 5 Установочные скрипты различных версий Liberica JDK

Различные схемы нумерации Raspberry Pi

Прежде чем подключать компоненты к разъемам GPIO (Интерфейс ввода/вывода общего назначения) на плате, изучите три схемы нумерации, используемые для идентификации пинов. Вас может запутать работа с разъемами GPIO. Детальную информацию можно найти в подробном руководстве по распиновке GPIO. Ниже приведена краткая сводка.

Нумерация Header Pin. Это логическая нумерация заголовка платы. Один ряд содержит четные пины, другой – нечетные.(см. Рис. 6)

<b>Рис. 6</b> Нумерация пинов заголовка платы
Рис. 6 Нумерация пинов заголовка платы

Нумерация BCM. Относится к номеру канала Broadcom – нумерации внутри микросхемы, используемой на Raspberry Pi.

Нумерация WiringPi. Wiring Pi – основной фреймворк, используемый Pi4J (в проекте он применяется в качестве библиотеки в Java) для управления GPIO. Причина другой схемы нумерации кроется в истории платы. Когда еще велась разработка первых моделей Raspberry Pi, предполагалось всего восемь контактов. В результате дальнейшего развития микрокомпьютера и добавления дополнительных контактов, нумерацию в WiringPi расширили, чтобы можно было на них ссылаться.

Чтобы Java разработчикам было проще понять разницу между различными типами заголовков, пинами и функциями, я разработал небольшую библиотеку, расположенную в репозитории Maven по адресу be.webtechie.pi-headers. Используя ее и небольшое приложение JavaFX, я сделал вспомогательную таблицу (см. Рис. 7), которая упрощает поиск и сопоставление номеров с соответствующими им пинами на плате. Более подробную информацию вы найдете по ссылке Raspberry Pi history, versions, pins and headers as a Java Maven library.”

<b>Рис. 7 </b>Сопоставление номеров с пинами на плате
Рис. 7 Сопоставление номеров с пинами на плате

Подключение оборудования

Давайте подключим оборудование, чтобы использовать часть мощностей платы Pi: светодиод, кнопку и датчик расстояния. См. Табл. 1, Рис. 8, Рис. 9.

<b>Табл. 1 </b>Сопоставление пинов с соответствующими устройствами
Табл. 1 Сопоставление пинов с соответствующими устройствами
<b>Рис. 8</b> Подключение проводов
Рис. 8 Подключение проводов
<b>Рис. 9</b> Схема подключения
Рис. 9 Схема подключения

На рис. 10 изображена система с использованием моста на макетной плате RasPiO, что упрощает поиск правильного пина. Разъем моста помещает номера BCM в логическом порядке, но я все еще использую отдельную макетную плату, чтобы было немного больше места. Portsplus предлагает аналогичное удобное решение.

<b>Рис. 10 </b>Фото системы с использованием моста макетной платы RasPiO
Рис. 10 Фото системы с использованием моста макетной платы RasPiO

Чтобы проверить, подключен ли светодиод в правильном положении с учетом его полярности, отсоедините кабель между светодиодом и пином GPIO (оранжевый кабель на рис. 10) и подключите его непосредственно к контакту 3,3 В (или к плюсу на плате). Если светодиод не загорится, вам нужно поменять его положение.

Проверьте светодиод и кнопку через терминал

Чтобы проверить соединение, запустите команду gpio через терминал.

Примечание: Если вы работаете с платой Raspberry Pi 4, обязательно используйте версию 2.52 утилиты gpio. Поскольку внутренняя проводка процессора на плате Pi 4 отличается от предыдущих моделей, при необходимости доступно обновление для утилиты. Проверьте свою версию с помощью команды gpio -v через терминал и, если нужно, установите новую с помощью следующих команд:

        $ gpio -v
gpio version: 2.50
$ cd /tmp
$ wget https://project-downloads.drogon.net/wiringpi-latest.deb
$ sudo dpkg -i wiringpi-latest.deb
$ gpio -v
gpio version: 2.52

    

Включить (1) и выключить (0) светодиод:

        $ gpio mode 29 out
$ gpio write 29 1
$ gpio write 29 0

    

Состояние кнопки (1=нажата, 2=не нажата) 27 пина WiringPi

        $ gpio mode 27 in 
$ gpio read 27
1

    

Переключение светодиода с помощью Java

Начинается самое интересное! Следующий код настраивает 29 пин WiringPi и 10 раз переключает его между включением и выключением с интервалом в 500 мс, используя ту же команду, которую я применял в терминале. Создайте файл под именем HelloGpio.java со следующим содержанием:

        public class HelloGpio {
    public static void main (String[] args) {
        System.out.println("Hello Gpio");

        try {
            Runtime.getRuntime().exec("gpio mode 29 out");

            var loopCounter = 0;
            var on = true;

            while (loopCounter < 10) {
                System.out.println("Changing LED to " + (on ? "on" : "off"));
                Runtime.getRuntime().exec("gpio write 29 " + (on ? "1" : "0"));

                on = !on;

                Thread.sleep(500);

                loopCounter++;
            }
        } catch (Exception ex) {
            System.err.println("Exception from Runtime: " + ex.getMessage());
        }
    }
}

    

Поскольку код будет использовать Java 11 (или выше), файл Java может быть выполнен без компиляции:

        $ java HelloGpio.java
Hello Gpio
Changing LED to on
Changing LED to off
Changing LED to on
…

    

Использование датчика движения

Написанное для этого блока приложение использует обычный ультразвуковой датчик расстояния, который можно найти во многих стартовых наборах Arduino и Pi. Это модуль под названием HC-SR04; дополнительную информацию и примеры вы можете найти в интернете. Модулю требуются входные и выходные GPIO-соединения. Датчик работает по той же схеме, что и система, используемая летучей мышью для полета в темноте: он использует отражение ультразвука для расчета расстояния до объекта.

Для измерения расстояния требуется, чтобы приложение и модуль выполнили следующие действия:

  1. Модуль нужно запитать на 5 V;
  2. Приложению нужно установить вывод trig в положение high как минимум на 10 мкс;
  3. Модуль посылает несколько (обычно восемь) сигналов 40 кГц и определяет, когда сигнал принимается обратно;
  4. Вывод echo устанавливается в положение high на ту же продолжительность, что и время, необходимое для возврата ультразвука к датчику;
  5. Приложение измеряет длительность положения high echo-вывода, чтобы рассчитать расстояние на основе скорости звука.

Готовое приложение

Запуск одного файла Java хорошо для тестирования системы, но это только первый шаг. В приложении используется библиотека Pi4J для объединения Java и портов GPIO на плате Raspberry Pi. Pi4J должен быть установлен на Raspberry Pi и может быть интегрирован в приложение Java с Maven. Полный исходный код примера вы можете найти на GitHub.

Следующие зависимости Maven указаны в файле POM:

Для JavaFX, расширенная зависимость javafx-web, которая включает:

javafx-controls – основу для приложения JavaFX;

Веб-компоненты, необходимые для TilesFX.

Логирование;

TilesFX для панели инструментов Tiles;

Pi4J для использования портов GPIO.

        <dependency>
    <groupId>org.openjfx</groupId>
    <artifactId>javafx-web</artifactId>
    <version>11.0.2</version>
</dependency>

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.13.1</version>
</dependency>

<dependency>
    <groupId>eu.hansolo</groupId>
    <artifactId>tilesfx</artifactId>
    <version>11.13</version>
</dependency>

<dependency>
    <groupId>com.pi4j</groupId>
    <artifactId>pi4j-core</artifactId>
    <version>1.2</version>
</dependency>

    

Вот классы для взаимодействия с оборудованием.

Класс GpioHelper. В нем сгруппированы функции, связанные с портами GPIO. Код начинается с определения контактов, к которым подключены компоненты оборудования, и инициализации Pi4J GpioController. Расширенная функциональность обрабатывается в отдельных классах: ButtonChangeEventListener и DistanceSensorMeasurement (описаны вкратце). Дополнительные методы и геттеры будут использоваться в UI позже.

        public class GpioHelper {

    private static final Logger logger = LogManager.getLogger(GpioHelper.class);

    /**
     * The pins being used in the example.
     */
    private static final Pin PIN_LED = RaspiPin.GPIO_29;        // BCM 21, Header pin 40
    private static final Pin PIN_BUTTON = RaspiPin.GPIO_27;     // BCM 16, Header pin 36
    private static final Pin PIN_ECHO = RaspiPin.GPIO_05;       // BCM 24, Header pin 18
    private static final Pin PIN_TRIGGER = RaspiPin.GPIO_01;    // BCM 18, Header pin 12

    /**
     * The connected hardware components.
     */
    private GpioController gpioController;

    /**
     * The Pi4J GPIO input and outputs.
     */
    private GpioPinDigitalOutput led = null;

    /**
     * The GPIO handlers.
     */
    private ButtonChangeEventListener buttonChangeEventListener = null;
    private DistanceSensorMeasurement distanceSensorMeasurement = null;

    /**
     * Constructor.
     */
    public GpioHelper() {
        try {
            // Initialize the GPIO controller
            this.gpioController = GpioFactory.getInstance();

            // Initialize the LED pin as a digital output pin with an initial low state
            this.led = gpioController.provisionDigitalOutputPin(PIN_LED, "RED", PinState.LOW);
            this.led.setShutdownOptions(true, PinState.LOW);

            // Initialize the input pin with pulldown resistor
            GpioPinDigitalInput button = gpioController
                    .provisionDigitalInputPin(PIN_BUTTON, "Button", PinPullResistance.PULL_DOWN);

            // Initialize the pins for the distance sensor and start thread
            GpioPinDigitalOutput trigger = gpioController.provisionDigitalOutputPin(PIN_TRIGGER, "Trigger", PinState.LOW);
            GpioPinDigitalInput echo = gpioController.provisionDigitalInputPin(PIN_ECHO, "Echo", PinPullResistance.PULL_UP);
            this.distanceSensorMeasurement = new DistanceSensorMeasurement(trigger, echo);
            ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
            executorService.scheduleAtFixedRate(this.distanceSensorMeasurement, 1, 1, TimeUnit.SECONDS);

            // Attach an event listener
            this.buttonChangeEventListener = new ButtonChangeEventListener();
            button.addListener(this.buttonChangeEventListener);
        } catch (UnsatisfiedLinkError | IllegalArgumentException ex) {
            logger.error("Problem with Pi4J! Probably running on non-Pi-device or Pi4J not installed. Error: {}",
                    ex.getMessage());
        }
    }

    public GpioController getGpioController() {
        return this.gpioController;
    }

    /**
     * Set the state of the LED.
     *
     * @param on Flag true if the LED must be switched on
     */
    public void setLed(boolean on) {
        if (this.led != null) {
            if (on) {
                this.led.high();
            } else {
                this.led.low();
            }
        }
    }

    /**
     * Get the data from the button.
     *
     * @return {@link XYChart.Series}
     */
    public XYChart.Series<String, Number> getButtonEvents() {
        if (this.buttonChangeEventListener != null) {
            return this.buttonChangeEventListener.getData();
        } else {
            return new Series<>();
        }
    }

    /**
     * Get the data from the distance measurement.
     *
     * @return {@link XYChart.Series}
     */
    public XYChart.Series<String, Number> getDistanceMeasurements() {
        if (this.distanceSensorMeasurement != null) {
            return this.distanceSensorMeasurement.getData();
        } else {
            return new Series<>();
        }
    }
}

    

Класс ButtonChangeEventListener. Поскольку этот класс реализует Pi4J GpioPinListenerDigital, он может обрабатывать изменение кнопки и сохранять изменение в XYChart.Series с отметкой времени.

        @Override
public void handleGpioPinDigitalStateChangeEvent(GpioPinDigitalStateChangeEvent event) {
    var timeStamp = LocalTime.now().format(DateTimeFormatter.ofPattern("HH.mm.ss"));
    this.data.getData().add(new XYChart.Data<>(timeStamp, event.getState().isHigh() ? 1 : 0));

    logger.info("Button state changed to {}", event.getState().isHigh() ? "high" : "low");
}

    

Класс DistanceSensorMeasurement. Этот класс является Runnable. Он добавляет отмеренное расстояние к аналогичной серии данных с отметкой времени для каждого пробега.

        @Override
public void run() {
    // Set trigger high for 0.01 ms
    this.trigger.pulse(10, PinState.HIGH, true, TimeUnit.NANOSECONDS);

    // Start the measurement
    while (this.echo.isLow()) {
        // Wait until the echo pin is high, indicating the ultrasound was sent
    }
    long start = System.nanoTime();

    // Wait until measurement is finished
    while (this.echo.isHigh()) {
        // Wait until the echo pin is low,  indicating the ultrasound was received back
    }
    long end = System.nanoTime();

    // Output the distance
    float measuredSeconds = getSecondsDifference(start, end);
    int distance = getDistance(measuredSeconds);
    logger.info("Distance is: {}cm for {}s ", distance, measuredSeconds);

    var timeStamp = new SimpleDateFormat("HH.mm.ss").format(new Date());
    this.data.getData().add(new XYChart.Data<>(timeStamp, distance));
}

    

UI. Благодаря TilesFX приложение в стиле дашборда может быть создано быстро. Полное построение интерфейса выполняется в классе DashboardScreen.java. В следующем фрагменте кода показана кнопка переключателя для включения и выключения светодиода:

        var ledSwitchTile = TileBuilder.create()
        .skinType(SkinType.SWITCH)
        .prefSize(200, 200)
        .title("LED")
        .roundedCorners(false)
        .build();

ledSwitchTile.setOnSwitchReleased(e -> gpioHelper.setLed(ledSwitchTile.isActive()));

    

Чтобы показать измерение расстояния, я использую tile типа SMOOTHED_CHART, который в свою очередь использует XYChart.Series из доступного через GpioHelper DistanceSensorMeasurement.

        var distanceChart = TileBuilder.create()
        .skinType(SkinType.SMOOTHED_CHART)
        .prefSize(500, 280)
        .title("Distance measurement")
        //.animated(true)
        .smoothing(false)
        .series(gpioHelper.getDistanceMeasurements())
        .build();

    

Класс приложения. Теперь все элементы готовы к объединению в класс приложения JavaFX. Ниже описано, как инициализировать GpioHelper и использовать его для инициализации DashboardScreen, а также есть дополнительный код, позволяющий красиво закрыть приложение.

        public class DashboardApp extends Application {

    private GpioHelper gpioHelper;

    @Override
    public void start(Stage stage) {
        Platform.setImplicitExit(true);

        this.gpioHelper = new GpioHelper();

        var scene = new Scene(new DashboardScreen(this.gpioHelper), 640, 480);
        stage.setScene(scene);
        stage.setTitle("JavaFX demo application on Raspberry Pi");
        stage.show();

        // Make sure the application quits completely on close
        stage.setOnCloseRequest(t -> CleanExit.doExit(this.gpioHelper.getGpioController()));
    }

    public static void main(String[] args) {
        launch();
    }

}

    

Запуск приложения на одноплатном компьютере Raspberry Pi

Вы можете запустить приложение на своем ПК через среду IDE, и UI отобразится, но большинство возможностей будут недоступны, потому что необходимы Pi4J и аппаратные компоненты. Перейдем к Raspberry Pi! Сначала вам нужно установить Pi4J. Это можно сделать с помощью однострочной команды:

$ curl -sSL https://pi4j.com/install | sudo bash

Финальный шаг – загрузить скомпилированный файл JAR с вашего ПК на плату Pi, что можно сделать с помощью SSH, USB-накопителя, загрузки или SD-карты. Поместите файл в /home/pi и запустите его с помощью java -jar:

        $ cd /home/pi
$ ls *.jar
javamagazine-javafx-example-0.0.1-jar-with-dependencies.jar
$ java -jar javamagazine-javafx-example-0.0.1-jar-with-dependencies.jar
INFO  be.webtechie.gpio.DistanceSensorMeasurement - Distance is: 103cm for 0.006021977s 
INFO  be.webtechie.gpio.DistanceSensorMeasurement - Distance is: 265cm for 0.01544218s 
INFO  be.webtechie.gpio.DistanceSensorMeasurement - Distance is: 198cm for 0.011520567s 
INFO  be.webtechie.gpio.ButtonChangeEventListener - Button state changed to high
INFO  be.webtechie.gpio.ButtonChangeEventListener - Button state changed to low

    

Сначала на экране отобразятся логи, а чуть позже откроется экран JavaFX. Диаграмма расстояний получает новое значение каждую секунду, а диаграмма кнопки обновляется при каждом ее нажатии. Светодиод можно переключать с помощью кнопки переключения на экране в одной из плиток. (см. Рис. 11)

<b>Рис. 11 </b>Фото запущенного приложения вместе с Raspberry Pi и другими компонентами
Рис. 11 Фото запущенного приложения вместе с Raspberry Pi и другими компонентами

Заключение

Как только вы узнаете, какую версию Java нужно использовать на Pi, вы можете очень быстро начать работу с простыми тестовыми программами и расширить их с помощью пользовательского интерфейса JavaFX. Создание сложного приложения требует дополнительных усилий, чтобы настроить все в вашей среде IDE и иметь возможность тестового запуска. Это позволяет вам легко разрабатывать программы на ПК и выполнять их на Pi.

Текст опубликован в переводе. Автор оригинальной статьи Frank Delporte.

***

Хочу научиться программировать с нуля, но не знаю, с чего начать. Что делать?

Можно учиться самостоятельно (долго) или пойти на курсы с преподавателями (быстро). Плюс нужно учитывать, что джунов много, конкуренция выше и работодатели повышают порог вхождения при найме на работу. Чтобы получить актуальные знания, мы в proglib.academy запустили курсы:

  • Основы программирования на Python.
  • Профессия Python-разработчик.
  • Алгоритмы и структуры данных.
  • Математика для Data Science.
  • Профессия Data Science.
  • Frontend Basic: принцип работы современного веба.
  • Профессия Фронтенд-разработчик.
  • Обработка естественного языка. Полный курс.

На подходе еще больше 10 курсов для взрослых и детей.

Источники

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ