Поскольку я становлюсь старшим разработчиком по возрасту, я перешел с одного языка на другой. Одним из моих главных интересов всегда были чистые, простые для понимания пользовательские интерфейсы (пользовательский интерфейс). Это путешествие началось для меня с Director (для создания мультимедийных компакт-дисков), Flash анимации веб-сайтов и Flex многофункциональных интернет-приложений (= “Flash на стероидах»).

Когда я начал разработку с Java более 10 лет назад, у нас было несколько проектов с ранними версиями Vaadin и JavaFX. . Когда я перешел к серверным приложениям, я продолжил использовать JavaFX только для некоторых личных и побочных проектов, и мне понравилось, как вы можете создавать пользовательский интерфейс как с XML (на самом деле FXML), так и с кодом, точно такой же подход, который мне нравился с Flex. С тех пор моя любовь к Java и JavaFX только росла, и это по-прежнему моя основная среда программирования.

Но у JavaFX недостает одной детали: запуска в браузере… Да, JPRO может это сделать, но для этого нужна лицензия и выделенный сервер. И да, есть некоторые текущие проекты, чтобы полностью перенести JavaFX в браузер, но они продолжаются и еще не созрели… Давайте рассмотрим другой подход: Vaadin Flow и запустим его на Raspberry Pi для управления светодиодом и отображения состояния кнопка.

О Ваадин

Только недавно я заново открыл для себя Vaadin для создания пользовательских веб-интерфейсов, и это действительно похоже на JavaFX в браузере. Полный код Java для создания ваших представлений и запуска его в браузере без необходимости писать ни одного файла HTML, CSS, JavaScript или TypeScript! Vaadin Flow – это уникальная платформа, позволяющая создавать современные (отзывчивые!) веб-приложения на 100 % на Java без написания кода HTML или JavaScript.

В этом руководстве я использую этот Vaadin Flow в сочетании с Pi4J для создания веб-интерфейса для взаимодействия со светодиодом и кнопкой, подключенными к контактам GPIO (ввод/вывод общего назначения) Raspberry Pi. Готовый проект доступен на GitHub.

О Pi4J

Pi4J, библиотека ввода-вывода Java для Raspberry Pi призвана объединить программирование на Java с электроникой. Проект стартовал в 2012 г. Версия 1.3 была выпущена в феврале 2021 г. для поддержки новейших плат Raspberry Pi (4, 400 и Compute 4) и является последней версией, основанной на Java 8. Летом 2021 г. Запущена версия 2, основанная на модулях Java 11, PiGpio, Java и т. д.

Используя Pi4J-зависимость в проекте, управление электронными компонентами, подключенными к контактам GPIO (ввод/вывод общего назначения) Raspberry Pi, можно контролировать как объекты в коде Java. Pi4J использует собственные библиотеки для управления GPIO, поэтому вам, как программисту, не нужно полностью осознавать всю магию, связанную с аппаратной связью.

Зачем запускать Java на Raspberry Pi?!

Целью проекта Raspberry Pi было создание недорогого ПК (от 15 долларов), доступного для всех. Вы можете подключить его к телевизору, если у вас нет экрана компьютера и доступны разные версии, в зависимости от вашего бюджета. Raspberry Pi — это полный Linux-ПК, предлагающий различные версии операционной системы Raspberry Pi, чтобы вы могли легко начать работу со всеми языками программирования, а также с Java.

Но главная причина, по которой я влюбился в Raspberry Pi, — это GPIO. Когда вы посмотрите на плату Raspberry Pi, вы обнаружите 40 контактов (2 ряда по 20). Это контакты GPIO (= ввод/вывод общего назначения). Некоторые из них могут использоваться как цифровой вход или выход, то есть 0 вольт отключен, 3,3 вольт включен. И количество вещей, которые вы можете сделать с ним, поражает! Эти контакты GPIO являются основными факторами, которые сделали Raspberry Pi таким успешным! Вы действительно можете найти дешевый подержанный ноутбук с аккумулятором, экраном и т. д., но вы никогда не сможете проводить эксперименты, сочетающие программное и аппаратное обеспечение, таким простым способом, как вы можете сделать это с Raspberry Pi.

И последний плюс Raspberry Pi: его мощность! 4-версия быстрая и имеет много памяти. Я написал об этом целую книгу и создал с ее помощью множество различных Java-приложений. Вы можете подключить до двух мониторов 4K, что означает много места для одновременного открытия разных приложений, терминалов, файлового менеджера и т. д.

Пример приложения Spring + Vaadin + Pi4J

Давайте создадим демонстрационный проект на основе Spring, чтобы проиллюстрировать, как пользовательский интерфейс Vaadin (веб-сайт) может взаимодействовать с GPIO Raspberry Pi с помощью библиотеки Pi4J. Взаимодействие GPIO основано на минимальном примере приложения Pi4J и использует кнопку и светодиод для демонстрации взаимодействия цифрового ввода и вывода. Проводка очень проста и требует всего несколько компонентов.

Базовый код был сгенерирован на start.vaadin.com/app со следующими параметрами:

  • Ваадин 23
  • Ява 17
  • Поток (только Java)
  • Дополнительная страница и пользовательские значки

Скачайте исходники, разархивируйте и откройте в предпочитаемой вами среде IDE. Это проект Maven, поэтому вы можете сразу же запустить его и проверить код, который был сгенерирован автоматически. При запуске приложение будет доступно по адресу https://localhost:8080 на самом устройстве.

Дополнительные зависимости

Поскольку мы хотим взаимодействовать с GPIO с помощью Pi4J, нам нужно добавить некоторые дополнительные зависимости в pom.xml:

  • pi4j-ядро
  • pi4j-плагин-raspberrypi
  • pi4j-плагин-pigpio

Сервис Pi4J

Поскольку у нас уже есть предварительно созданное приложение Spring, мы можем легко расширить его с помощью службы для управления всеми методами, связанными с Pi4J. Начнем с инициализации контекста Pi4J, который отвечает за все взаимодействия GPIO.

private final Context pi4j;
private static final int PIN_BUTTON = 24; // PIN 18 = BCM 24
private static final int PIN_LED = 22; // PIN 15 = BCM 22
private final Queue<ButtonListener> buttonListeners;
private DigitalOutput led;
public Pi4JService() {
    pi4j = Pi4J.newAutoContext();
    buttonListeners = new ConcurrentLinkedQueue<>();
    initLed();
    initButton();
}

Получив контекст, мы можем использовать его для настройки и инициализации DigitalOutput для светодиода и DigitalInput для кнопки. В нашем примерном приложении у нас есть только один компонент, который изменяется в зависимости от состояния кнопки, но с помощью интерфейса ButtonListener и списка реализаций мы можем связать неограниченное количество компонентов с состоянием кнопки.

private void initLed() {
    try {
        var ledConfig = DigitalOutput.newConfigBuilder(pi4j)
                .id("led")
                .name("LED")
                .address(PIN_LED)
                .shutdown(DigitalState.LOW)
                .initial(DigitalState.LOW)
                .provider("pigpio-digital-output");
        led = pi4j.create(ledConfig);
        logger.info("The LED has been initialized on pin {}", PIN_LED);
    } catch (Exception ex) {
        logger.error("Error while initializing the LED: {}", ex.getMessage());
    }
}
/**
* Toggle the LED on or off.
*
* @param on
*/
public void setLedState(boolean on) {
    led.setState(on);
}
private void initButton() {
    try {
        var buttonConfig = DigitalInput.newConfigBuilder(pi4j)
                .id("button")
                .name("Button")
                .address(PIN_BUTTON)
                .pull(PullResistance.PULL_DOWN)
                .debounce(3000L)
                .provider("pigpio-digital-input");
        var button = pi4j.create(buttonConfig);
        button.addListener(e -> {
            logger.info("Button state changed to {}", e.state());
            buttonListeners.forEach(bl -> bl.onButtonEvent(e.state()));
        });
        logger.info("The button has been initialized on pin {}", PIN_BUTTON);
    } catch (Exception ex) {
        logger.error("Error while initializing the button: {}", ex.getMessage());
    }
}
/**
* Add a button listener which will get all state changes of the button DigitalInput
*
* @param buttonListener
*/
public void addButtonListener(ButtonListener buttonListener) {
    buttonListeners.add(buttonListener);
}

Кроме того, есть несколько вспомогательных методов для отображения состояния библиотеки Pi4J. Это только один из них, пожалуйста, проверьте источники для полных реализаций:

/**
* Providers are intended to represent I/O implementations and provide access to the I/O interfaces available on
* the system. Providers 'provide' concrete runtime implementations of I/O interfaces.
*/
public String getProviders() {
    if (pi4j == null || pi4j.providers() == null) {
        return "None";
    }
    return pi4j.providers().all().entrySet().stream()
            .map(e -> e.getKey() + ": " + e.getValue())
            .collect(Collectors.joining(","));
}

Пользовательский интерфейс для переключения светодиода

Теперь давайте изменим первую страницу для управления светодиодом. Код на самом деле очень ограничен, так как мы используем Checkbox и добавляем прослушиватель значений, чтобы указать нашему Pi4JService изменить состояние светодиода.

public class ButtonView extends HorizontalLayout implements ButtonListener {
Logger logger = LoggerFactory.getLogger(ButtonView.class);
private final UI ui;
    private final Label lbl;
public ButtonView(@Autowired Pi4JService pi4JService) {
        ui = UI.getCurrent();
        lbl = new Label("Waiting for button change...");
        add(lbl);
setMargin(true);
        setVerticalComponentAlignment(Alignment.END, lbl);
pi4JService.addButtonListener(this);
    }
@Override
    public void onButtonEvent(DigitalState state) {
        var isPressed = state.equals(DigitalState.HIGH);
        logger.info("Button event in listener: {} - Is on: {}", state, isPressed);
        ui.accessSynchronously(() -> lbl.setText(isPressed ? "Button is pressed" : "Button is released"));
    }
}

Пользовательский интерфейс для просмотра состояния кнопки

Этот пользовательский интерфейс имеет немного больше кода. Он расширяет ButtonListener, поэтому необходимо переопределить onButtonEvent, но другие части кода говорят сами за себя. Здесь следует отметить еще одну вещь: нам нужно использовать ui.accessSynchronously для изменения компонента, который является частью представления, поскольку Pi4JService и пользовательский интерфейс работают в отдельных потоках.

public class ButtonView extends HorizontalLayout implements ButtonListener {
Logger logger = LoggerFactory.getLogger(ButtonView.class);
private final UI ui;
    private final Label lbl;
public ButtonView(@Autowired Pi4JService pi4JService) {
        ui = UI.getCurrent();
        lbl = new Label("Waiting for button change...");
        add(lbl);
setMargin(true);
        setVerticalComponentAlignment(Alignment.END, lbl);
pi4JService.addButtonListener(this);
    }
@Override
    public void onButtonEvent(DigitalState state) {
        var isPressed = state.equals(DigitalState.HIGH);
        logger.info("Button event in listener: {} - Is on: {}", state, isPressed);
        ui.accessSynchronously(() -> lbl.setText(isPressed ? "Button is pressed" : "Button is released"));
    }
}

Необходимо дополнительное изменение в основном классе. Поскольку мы хотим отправить изменения из backend в user interface, нам нужно добавить атрибут @Push. Это крошечное изменение позволяет вам обновлять пользовательский интерфейс с сервера без явного запроса пользователем обновлений. Это основано на соединении клиент-сервер (WebSocket, если поддерживается, или альтернативном), которое клиент устанавливает, а сервер может затем использовать для отправки обновлений клиенту.

@SpringBootApplication
@Theme(value = "pi4jdemo")
@NpmPackage(value = "line-awesome", version = "1.3.0")
@Push
public class Application extends SpringBootServletInitializer implements AppShellConfigurator {

Пользовательский интерфейс с информацией Pi4J

Чтобы иметь возможность отлаживать, как Pi4J взаимодействует с GPIO, на экран «О программе» добавляется некоторая дополнительная информация, чтобы показать загруженные платформы и провайдеры. Реестр содержит список всех инициализированных входов и выходов, поэтому мы ожидаем увидеть здесь светодиод и кнопку.

public class AboutView extends VerticalLayout {
    public AboutView(@Autowired Pi4JService pi4JService) {
        setSpacing(false);
        add(
            new H2("Java"),
            new Paragraph("Version: " + System.getProperty("java.version")),
            new H2("Pi4J"),
            new Paragraph("Default platform: " + pi4JService.getDefaultPlatform()),
            new Paragraph("Loaded platforms: " + pi4JService.getLoadedPlatforms()),
            new Paragraph("Providers: " + pi4JService.getProviders()),
            new Paragraph("Registry: " + pi4JService.getRegistry())
        );
        setSizeFull();
        getStyle().set("text-align", "left");
    }
}

Сборка и загрузка на Raspberry Pi

Если вы разрабатываете на ПК, вы можете собрать приложение с помощью следующей команды

  • Windows 7_
  • Mac и Linux: ./mvnw clean package -Pproduction

и загрузите на свой Raspberry Pi с помощью следующей команды (замените логин pi и IP-адрес на адрес вашей платы):

$ scp target/pi4jdemo-1.0-SNAPSHOT.jar [email protected]://home/pi/

Запуск на Raspberry Pi

Теперь мы можем запустить приложение на нашем Raspberry Pi, но нам нужно запустить его с sudo, поскольку Pi4J (все еще) нуждается в этом, чтобы иметь возможность взаимодействовать с GPIO, используя PiGpio в качестве родной библиотеки.

$ sudo java -jar /home/pi/pi4jdemo-1.0-SNAPSHOT.jar

Если вы установили Java с помощью SDKMAN как обычный пользователь, вы, вероятно, получите эту ошибку:

sudo: java: command not found

Один из способов исправить это — позволить самой команде искать, где установлена ​​Java, с помощью `which java`:

$ sudo `which java` -jar /home/pi/pi4jdemo-1.0-SNAPSHOT.jar
____   _  _  _        _   ____                          
|  _ \ (_)| || |      | | |  _ \   ___  _ __ ___    ___  
| |_) || || || |_  _  | | | | | | / _ \| '_ ` _ \  / _ \ 
|  __/ | ||__   _|| |_| | | |_| ||  __/| | | | | || (_) |
|_|    |_|   |_|   \___/  |____/  \___||_| |_| |_| \___/
INFO 1807 --- [           main] be.webtechie.vaadin.pi4j.Application     : Starting Application v1.0-SNAPSHOT using Java 17.0.2 on 64bit with PID 1807 (/home/pi/pi4jdemo-1.0-SNAPSHOT.jar started by root in /home/pi)
INFO 1807 --- [           main] be.webtechie.vaadin.pi4j.Application     : No active profile set, falling back to 1 default profile: "default"
INFO 1807 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
INFO 1807 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
INFO 1807 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.58]
INFO 1807 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
INFO 1807 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 6486 ms
INFO 1807 --- [           main] c.v.f.s.VaadinServletContextInitializer  : Search for VaadinAppShell took 125 ms
INFO 1807 --- [           main] com.pi4j.Pi4J                            : New auto context
INFO 1807 --- [           main] com.pi4j.Pi4J                            : New context builder
INFO 1807 --- [           main] c.p.p.impl.DefaultRuntimePlatforms       : adding platform to managed platform map [id=raspberrypi; name=RaspberryPi Platform; priority=5; class=com.pi4j.plugin.raspberrypi.platform.RaspberryPiPlatform]
INFO 1807 --- [           main] b.w.vaadin.pi4j.service.Pi4JService      : The LED has been initialized on pin 22
INFO 1807 --- [           main] b.w.vaadin.pi4j.service.Pi4JService      : The button has been initialized on pin 24
INFO 1807 --- [           main] c.v.f.s.DefaultDeploymentConfiguration   : Vaadin is running in production mode.
INFO 1807 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
INFO 1807 --- [           main] be.webtechie.vaadin.pi4j.Application     : Started Application in 14.089 seconds (JVM running for 17.643)
INFO 1807 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
INFO 1807 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
INFO 1807 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 3 ms
INFO 1807 --- [nio-8080-exec-1] c.vaadin.flow.spring.SpringInstantiator  : The number of beans implementing 'I18NProvider' is 0. Cannot use Spring beans for I18N, falling back to the default behavior
INFO 1807 --- [       Thread-7] b.w.vaadin.pi4j.service.Pi4JService      : Button state changed to LOW
INFO 1807 --- [       Thread-7] b.w.vaadin.pi4j.views.button.ButtonView  : Button event in listener: LOW - Is on: false
INFO 1807 --- [       Thread-8] b.w.vaadin.pi4j.service.Pi4JService      : Button state changed to HIGH
INFO 1807 --- [       Thread-8] b.w.vaadin.pi4j.views.button.ButtonView  : Button event in listener: HIGH - Is on: true

В этом видео вы можете увидеть журналы работающего приложения на Raspberry Pi, браузера на ПК в той же сети, чтобы проиллюстрировать отзывчивый веб-интерфейс, а также вид камеры на макетную плату и Raspberry Pi со светодиодом и кнопкой:

Заключение

Благодаря стартовому сайту, предоставленному Vaadin, вы можете получить полностью работающее приложение в несколько кликов. Добавление GPIO-взаимодействия легко с библиотекой Pi4J и позволяет вам полностью взаимодействовать с электронными компонентами, подключенными к контактам GPIO Raspberry Pi. Используя исходные коды этой статьи, вы можете легко начать работу и расширить ее с помощью других типов компонентов.

Первоначально опубликовано на https://dzone.com.