Kaffee und Kuchen – Projekte mit Java Embedded 8 auf dem Raspberry Pi

Keine Kommentare

Wer vielleicht auch wie der Autor in den frühen 1980er Jahren mit dem Home-Computing eingestiegen ist und auch damals bereits gerne mit Elektronik gebastelt und programmiert hat, wird sich bestimmt auch über die kleinen preiswerten Minicomputer gefreut haben, die seit einiger Zeit auf dem Markt sind.

Ursprünglich war der Raspberry Pi (RPi), der im Jahr 2012 auf den Markt kam, als Lehrplattform für digitale Elektronik auf Basis der Sprache Python konzipiert (daher das “Pi“ Namen, „Python interpreter“). Der Python interpreter sollt ursprünglich fest eingebaut sein, aber am Ende wurde es dann doch eine sehr offene Architektur und so hat sich um dieses Gerät sehr schnell eine recht große Community gebildet.

Bis heute wurden bereits mehr als 5 Millionen Stück verkauft. Dieser hohe Verbreitungsgrad und die damit verbundene sehr breite Unterstützung machen den Einstieg ins digitale Experimentieren sehr leicht und vor allem preiswert: Ein aktueller Raspberry Pi 2 Model B mit Gehäuse, Netzteil, WLAN-Stick und SD-Karte liegt bei 70-80 Euro.

Auch Java-Entwickler kommen hier inzwischen voll auf ihre Kosten, denn seit geraumer Zeit bringt Oracle regelmäßig parallel zu den „normalen“ JDK 8 Builds jeweils ein JDK 8 for ARM, dass speziell für ARM basierte Plattformen, wie den Raspberry Pi gedacht ist.
Anfangs wurde die Unterstützung von JavaFX für den Einsatz von grafischen Elementen explizit von Oracle beworben. Mit dem aktuellen „Update 33“-Release wurde aber „klamm und heimlich“ JavaFX aus den offiziellen Builds ausgeklammert und dem OpenJDK Projekt übergeben. Johan Vos (Gluon) hat glücklicherweise gleich ein Paket zusammengestellt, dass vom „javafxports“-Repository herunterladen und extrahieren werden kann. Die darin verpackten Bibliotheken lassen sich einfach in entsprechende Verzeichnisse eines installierten ARM-JDKs von Oracle kopieren. Damit steht JavaFX für Embedded wieder zur Verfügung.

Bisher waren die Gestaltungsmöglichkeiten von JavaFX-Oberflächen auf dem RPi Aufgrund eingeschränkter Leistung überschaubar. Vor kurzem ist jedoch der Raspberry Pi 2 mit QuadCore Prozessor und 1GB RAM erschienenen und damit wird es wieder interessanter, UIs für den RPi zu entwickeln:

Da JavaFX als Besonderheit direkt im Framebuffer läuft, ist kein laufender X-Server erforderlich und es können deutlich Ressourcen gespart werden.

Dieser Artikel zeigt exemplarisch, wie sich Eingangs- und Ausgangszustände eines Raspberry Pi über eine JavaFX basierte Touch-Oberfläche manipulieren und visualisieren lassen.

NetBeans 8

NetBeans 8 hält für Internet-of-Things-Java-Entwickler ein sehr beachtenswertes Feature bereit, das bereits in der Standardinstallation voll integriert zur Verfügung steht:
die Möglichkeit, direkt aus der IDE ein Projekt auf ein Embedded Device, wie einen Raspberry Pi einzurichten und dort remote auszuführen. Dabei kann das Projekt auch im Debug-Modus ausführt oder mit dem Profiler zur Laufzeit überwacht werden.
Vorausgesetzt auf dem entfernten Gerät läuft ein SSH-Dienst, kann eine neue Plattform vom Typ „Remote Java Standard Edition“ erstellt werden. Hier sind dann im folgenden Dialog Informationen wie Adresse des Gerätes, Login Credentials und Pfad zum zu benutzenden JRE/JDK hinterlegt. Danach kann diese in den Projekteinstellungen diese dann als Zielplattform ausgewählt und dann das Projekt laufen gelassen werden.

01_Dialog_Remote_Platforms     
Abbildung 1:
Remote Platform in NetBeans 8.0.2

Dabei wird das Programm mit allen Abhängigkeiten lokal gebaut, per SCP übertragen und remote via SSHExec ausgeführt. Die Ausgaben von stdout und stderr werden schließlich in die Konsole von NetBeans umgeleitet. Das Remote-Deployment dient nicht nur zur entfernten Ausführung sondern kann eben auch als Auslieferung verstanden werden, denn die Applikation wird anschließend nicht vom RPi gelöscht.
Besonders betont sei hier noch das Setzen von „sudo“ als „Exec Prefix“ Property. Diese Einstellung sorgt dafür, dass das Java-Programm mit „Root“-Rechten auf dem entfernten Gerät ausgeführt wird. Wenn keine anderweitigen Gruppen- und Rechteumstellungen auf dem Raspberry Pi einrichtet werden sollen, ist dies unbedingt nötig, um auf die GPIO-Schnittstelle zugreifen zu dürfen.
Da all diese Schritte ANT basiert sind, kann hier leider nicht auf Maven-Repositories zugegriffen werden. Es ist also erforderlich, Bibliotheken und Abhängigkeiten manuell mit NetBeans-Bordmitteln zu verwalten.

GPIO und Pi4J

Zunächst ein Blick auf die General Purpose Input/Output (GPIO) Schnittstelle des Raspberry Pi. Sie ist das IO-Interface des RPi deren Funktionen sich sehr leicht mit wiringPi (eine C basierte API von Gordon Henderson (@drogon)) nutzen lassen. Praktischer Weise gibt es mit Pi4J eine Java API, das genau auf wiringPi aufsetzt und dieses auch noch passend automatisch mitbringt.
Der GPIO-Header steht im Mittelpunkt der ausgehenden und eingehenden Kommunikation. Beim Modell B können insgesamt 17 Pins zum I/O angefordert werden: 8 Pins GPIO Pins, 2 Pins vom I2C Interface, 5 Pins Serial Peripheral Interface, 2 Pins Serial UART (+ 4 weitere via P5 Connector (nur Rev. 2.0)). Das neuere Modell B+ hält noch weitere 9 GPIOs bereit.
Pi4J unterstützt alle RPi Modelle vom einfachem I/O über PWM und SPI bis I2C bleiben keine Wünsche offen.

Pi4J als Bibliothek einrichten

Zunächst lädt und entpackt man ein aktuelles Build, z.B. den Pi4J 1.0 Release Candidate von der Pi4J Site. Anschließend kann dann in NetBeans eine Bibliothek eingerichtet werden.

Beispiel-Projekt

Für das folgende Beispiel-Projekt „8 Kanal I/O Interface mit JavaFX basierter Touch-Oberfläche“ werden folgende Bauteil benötigt:

  • Raspberry Pi (B oder B+)
  • Breakout-Kit
  • Zwei Breadboards
  • Steckverbinder
  • 7“ Touch-Display von Chalk-Elec [10]
  • Acht LEDs
  • Acht 330 Ω Vorwiderstände
  • Acht Taster
02_Fritzing_Sketch_Steckplatine
DSC_5639
Abbildung 2:
Schaltungsaufbau der 8 Kanal I/O Steckplatine

Die Zustände für die Ausgänge werden über ToggleButtons der grafischen Oberfläche manipuliert, die Eingänge über Taster getriggert und die Zustände in beiden Fällen an der UI angezeigt. Der I/O-Modus (Eingang/Ausgang) kann jeweils pro Kanal zur Laufzeit umgeschaltet werden (Abbildung 3 bis 5).

Zum besseren Testen der Oberflache kann zudem der GPIO Controller wahlweise aktiviert oder deaktiviert werden. Außerdem darf ein „Exit“-Button nicht vergessen werden: JavaFX kann direkt aus der Konsole den FrameBuffer übernehmen und die Anwendung kann dann nicht ohne Weiteres beendet werden (CTRL-D ist wirkungslos, weil JavaFX Keyboard-Events abfängt).

03_RasPiFX-GUI-1
Abbildung 3:
Das JavaFX UI zum 8 Kanal I/O
04_RasPiFX-GUI-2
Abbildung 4:
Über das UI kann zum Beispiel auch der I/O Modus pro Kanal (IN/OUT) gewählt werden
05_Schalteinheit_Platine_vs_UI
Abbildung 5:
Ein Kanal der Schaltung wird in der UI durch eine Spalteneinheit repräsentiert

Für die UI soll die eigentliche GPIO Kommunikation transparent sein. Daher werden alle Zustände über einen JavaFX-Properties-Adapter gekapselt. Diese Zwischenschicht bildet die Verbindung von UI und einer Einheit, die über Pi4J das GPIO Interface anspricht.

06_Architektur
Abbildung 5:
Die Architektur des Datenflusses vom Taster zur UI und zurück

Etwas gekürzter Auszug der Klasse GpioAdapter.java (es wurde der Code zu Kanal 1-7 ausgeblendet):

public class GpioAdapter {
 
    private GpioController gpio;
    private GpioPinDigitalMultipurpose pin0;
	[] 
   private ObjectProperty gpio0ModeProperty;
	[] 
    private BooleanProperty gpio0StateProperty;
	[] 
    private BooleanProperty connectedProperty;
    private Timeline testTimeline;
    private GpioPinDigitalMultipurpose[] pins;
    private BooleanProperty[] stateProperties;
    private ObjectProperty[] modeProperties;
    private final static Logger LOGGER = Logger.getLogger(GpioAdapter.class.getName());
 
    public GpioAdapter() {
        init();
    }
 
    private void init() {
        connectedProperty = new SimpleBooleanProperty(Boolean.FALSE);
        gpio0StateProperty = new SimpleBooleanProperty(Boolean.FALSE);
	[] 
        stateProperties = new BooleanProperty[]{gpio0StateProperty,
            gpio1StateProperty,
	[] 
        };
        gpio0ModeProperty = new SimpleObjectProperty<>(PinMode.DIGITAL_OUTPUT);
	[] 
        modeProperties = new ObjectProperty[]{
            gpio0ModeProperty,
	[] 
        };
    }
 
    private ChangeListener createPinStatePropertyListener(final GpioPinDigitalMultipurpose pin) {
        return (ObservableValue<? extends Boolean> ov, Boolean oldValue, Boolean newValue) -> {
            LOGGER.log(Level.INFO, "pinPropertyChanged: {0} {1}", new Object[]{pin.getName(), newValue});
            if (pin.getMode() != PinMode.DIGITAL_INPUT) {
                if (newValue) {
                    pin.high();
                } else {
                    pin.low();
                }
            }
        };
    }
 
    private void addGpioInputListener(final GpioPinDigitalMultipurpose pin, final BooleanProperty gpioStateProperty) {
        pin.addListener((GpioPinListenerDigital) new GpioPinListenerDigital() {
            @Override
            public void handleGpioPinDigitalStateChangeEvent(final GpioPinDigitalStateChangeEvent event) {
                LOGGER.log(Level.INFO, "pinstateChanged: {0} {1}", new Object[]{pin.getName(), event.getState()});
                Platform.runLater(() -> {
                    gpioStateProperty.set(event.getState().
                            isHigh());
                });
            }
        });
    }
 
    /*
     * -------------------------- ACTIONS -------------------------- 
     */
    public void connect() {
        LOGGER.log(Level.INFO, "connect...");
 
        gpio = GpioFactory.getInstance();
        pin0 = gpio.provisionDigitalMultipurposePin(RaspiPin.GPIO_00, gpio0ModeProperty.get(), PinPullResistance.PULL_DOWN);
	[] 
 
        pins = new GpioPinDigitalMultipurpose[]{
            pin0, pin1, pin2, pin3, pin4, pin5, pin6, pin7
        };
        gpio.setShutdownOptions(true, PinState.LOW, pins);
 
        addGpioInputListener(pin0, gpio0StateProperty);
	[] 
 
        gpio0StateProperty.addListener(createPinStatePropertyListener(pin0));
	[] 
 
        reset();
        setConnectedPropertyValue(Boolean.TRUE);
        LOGGER.log(Level.INFO, "connected.");
 
    }
 
    public void setOnAllPins() {
        LOGGER.log(Level.INFO, "setOnAllPins()");
        for (int i = 0; i <= 7; i++) {
            stateProperties[i].setValue(Boolean.TRUE);
        }
    }
 
    public void setAllPinsLow() {
        LOGGER.log(Level.INFO, "setOffAllPins()");
        for (int i = 0; i <= 7; i++) {
            stateProperties[i].setValue(Boolean.FALSE);
        }
    }
 
    public void disconnect() {
        LOGGER.log(Level.INFO, "disconnect()");
        if (gpio != null) {
            gpio.shutdown();
        }
        setConnectedPropertyValue(Boolean.FALSE);
 
    }
 
    public void resetIOModes() {
        LOGGER.log(Level.INFO, "resetIOModes()");
        for (int i = 0; i <= 7; i++) {             setGpioMode(i, PinMode.DIGITAL_OUTPUT);         }     }     public void reset() {         LOGGER.log(Level.INFO, "reset()");         if (testTimeline != null) {             testTimeline.stop();         }         setAllPinsLow();         resetIOModes();     }     public void connectTest() {         LOGGER.log(Level.INFO, "connectTest()");         if (testTimeline != null) {             testTimeline.stop();         }         reset();         testTimeline = new Timeline(new KeyFrame(Duration.seconds(1), (ActionEvent event) -> {
            setOnAllPins();
        }), new KeyFrame(Duration.seconds(0.1), (ActionEvent event) -> {
            setAllPinsLow();
        }));
        testTimeline.play();
    }
 
    public void test(double millis) {
        LOGGER.log(Level.INFO, "test()");
        reset();
        testTimeline = new Timeline(new KeyFrame(Duration.millis(millis), (ActionEvent event) -> {
            gpio0StateProperty.setValue(Boolean.TRUE);
        }), new KeyFrame(Duration.millis(millis * 2), (ActionEvent event) -> {
            gpio1StateProperty.setValue(Boolean.TRUE);
        }), new KeyFrame(Duration.millis(millis * 3), (ActionEvent event) -> {
            gpio2StateProperty.setValue(Boolean.TRUE);
        }), new KeyFrame(Duration.millis(millis * 4), (ActionEvent event) -> {
            gpio3StateProperty.setValue(Boolean.TRUE);
        }), new KeyFrame(Duration.millis(millis * 5), (ActionEvent event) -> {
            gpio4StateProperty.setValue(Boolean.TRUE);
        }), new KeyFrame(Duration.millis(millis * 6), (ActionEvent event) -> {
            gpio5StateProperty.setValue(Boolean.TRUE);
        }), new KeyFrame(Duration.millis(millis * 7), (ActionEvent event) -> {
            gpio6StateProperty.setValue(Boolean.TRUE);
        }), new KeyFrame(Duration.millis(millis * 8), (ActionEvent event) -> {
            gpio7StateProperty.setValue(Boolean.TRUE);
        }));
        testTimeline.play();
    }
 
    /*
     * -------------------------- PROPERTY METHODS -------------------------- 
     */
    public void setGpioStateValue(int pinNumber, Boolean state) {
        stateProperties[pinNumber].setValue(state);
    }
 
    public void setGpioMode(int pinNumber, PinMode mode) {
        modeProperties[pinNumber].setValue(mode);
        if (isConnected()) {
            pins[pinNumber].setMode(mode);
            if (PinMode.DIGITAL_OUTPUT.equals(mode)) {
                pins[pinNumber].setState(PinState.LOW);
            }
        }
    }
 
    public void setConnectedPropertyValue(Boolean connected) {
        this.connectedProperty.setValue(connected);
    }
 
    public boolean isConnected() {
        return connectedProperty.get();
    }
 
    public BooleanProperty connectedProperty() {
        return connectedProperty;
    }
 
    public BooleanProperty gpio0StateProperty() {
        return gpio0StateProperty;
    }
	[] 
   public ObjectProperty gpio0ModeProperty() {
        return gpio0ModeProperty;
    }
 	[] 
}

Etwas gekürzter Auszug der Klasse IOBoard.java (der Gpio-UI-Controller, (es wurde der Code zu Kanal 1-7 ausgeblendet):

public class IOBoard extends VBox {
 
    private final static Logger LOGGER = Logger.getLogger(IOBoard.class.getName());
    @FXML
    private GridPane buttonGridPane;
    @FXML
    private ToggleButton toogleGPIO0;
    @FXML
	[] 
    @FXML
    private ToggleButton toggleModeGPIO0;
	[] 
    @FXML
    private Button exitButton;
    @FXML
    private VBox indicatorBox0;
    @FXML
  	[] 
   @FXML
    private ToggleButton gpioConnectToggleButton;
    @FXML
    private ResourceBundle resources;
 
    private GpioAdapter gpioAdapter;
    private Indicator indicatorGPIO0;
	[] 
   private ToggleButton[] toggleGPIOButtons;
    private ToggleButton[] toggleGPIOModeButtons;
 
    public IOBoard() {
        init();
    }
 
    private void init() {
        ResourceBundle resourceBundle = ResourceBundle.getBundle(getClass().getPackage().getName() + ".ioboard");
        FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("ioboard.fxml"));
        fxmlLoader.setResources(resourceBundle);
        fxmlLoader.setRoot(this);
        fxmlLoader.setController(this);
        try {
            fxmlLoader.load();
        } catch (IOException ex) {
            LOGGER.log(Level.SEVERE, null, ex);
        }
        AwesomeDude.setIcon(exitButton, AwesomeIcon.POWER_OFF, "2em");
        gpioAdapter = new GpioAdapter();
        indicatorGPIO0 = createIndicator();
	[] 
 
        indicatorBox0.getChildren().add(indicatorGPIO0);
	[] 
 
        indicatorGPIO0.passProperty().bindBidirectional(gpioAdapter.gpio0StateProperty());
	[] 
 
        gpioAdapter.gpio0ModeProperty().addListener(new IOModeChangeEventHandler(toggleModeGPIO0));
	[] 
 
        toggleModeGPIO0.setOnAction(new ToggleModeEventHandler(toggleModeGPIO0, 0));
	[] 
 
        toogleGPIO0.visibleProperty().bind(toggleModeGPIO0.selectedProperty().not());
	[] 
 
        toogleGPIO0.selectedProperty().bindBidirectional(gpioAdapter.gpio0StateProperty());
	[] 
 
        toggleGPIOButtons = new ToggleButton[]{
            toogleGPIO0,
	[] 
        };
 
        toggleGPIOModeButtons = new ToggleButton[]{
            toggleModeGPIO0,
	[] 
        };
 
        gpioConnectToggleButton.selectedProperty()
                .addListener((ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean selected) -> {
                    if (!OS.isLinux()) {
                        LOGGER.info("Obviously not running on a Raspberry Pi. GPIO is not going to be connected.");
                        gpioConnectToggleButton.setSelected(false);
                        return;
                    }
                    if (selected) {
                        LOGGER.info("GPIO Connect");
                        gpioConnectToggleButton.setText(resources.getString("button.gpio.connected"));
                        onGpioConnect();
                    } else {
                        LOGGER.info("GPIO Disconnect");
                        gpioConnectToggleButton.setText(resources.getString("button.gpio.disconnected"));
                        onGpioDisconnect();
                    }
                }
                );
 
        onReset();
    }
 
    private Indicator createIndicator() {
        Indicator indicator = new Indicator();
        indicator.setResult(Indicator.Result.FAIL);
        indicator.setPrefSize(100.0, 100.0);
        return indicator;
    }
 
    /*
     * -------------------------- ACTIONS -------------------------- 
     */
    @FXML
    public void onTest() {
        LOGGER.info("onTest");
        for (ToggleButton toggleGPIOModeButton : toggleGPIOModeButtons) {
            toggleGPIOModeButton.setSelected(false);
        }
        gpioAdapter.test(1000);
    }
 
    @FXML
    public void onReset() {
        LOGGER.info("onReset");
        for (ToggleButton toggleGPIOModeButton : toggleGPIOModeButtons) {
            toggleGPIOModeButton.setSelected(false);
        }
        gpioAdapter.reset();
    }
 
    public void onGpioDisconnect() {
        LOGGER.info("onGpioDisconnect");
        gpioAdapter.disconnect();
    }
 
    public void onGpioConnect() {
        LOGGER.info("onGpioConnect");
        gpioAdapter.connect();
    }
 
    @FXML
    public void onExit() {
        LOGGER.info("onExit");
        Platform.exit();
        System.exit(0);
 
    }
 
    private class ToggleModeEventHandler implements EventHandler {
 
        private final ToggleButton button;
        private final int pinNumber;
 
        public ToggleModeEventHandler(final ToggleButton button, final int pinNumber) {
            this.button = button;
            this.pinNumber = pinNumber;
        }
 
        @Override
        public void handle(ActionEvent t) {
            toggleGPIOButtons[pinNumber].setSelected(false);
            if (button.isSelected()) {
                LOGGER.log(Level.INFO, "set Pin: {0} Mode: {1}", new Object[]{pinNumber, PinMode.DIGITAL_INPUT});
                gpioAdapter.setGpioMode(pinNumber, PinMode.DIGITAL_INPUT);
 
            } else {
                LOGGER.log(Level.INFO, "set Pin: {0} Mode: {1}", new Object[]{pinNumber, PinMode.DIGITAL_OUTPUT});
                gpioAdapter.setGpioMode(pinNumber, PinMode.DIGITAL_OUTPUT);
            }
        }
    }
 
    private class IOModeChangeEventHandler implements ChangeListener {
 
        private final ToggleButton modeToggleButton;
 
        public IOModeChangeEventHandler(final ToggleButton modeToggleButton) {
            this.modeToggleButton = modeToggleButton;
            modeToggleButton.setText("OUT");
        }
 
        @Override
        public void changed(ObservableValue<? extends PinMode> ov, PinMode t, PinMode newMode) {
            if (newMode.equals(PinMode.DIGITAL_OUTPUT)) {
                modeToggleButton.setText("OUT");
            } else {
                modeToggleButton.setText("IN");
            }
        }
    }
 
}

Der Code des Projektes (RaspiGPIOControllerFX) kann via BitBucket bezogen werden.
Zudem gibt es noch folgende YouTube Videos zum Thema NetBeans Remote Deployment, die auch für den Oracle Virtual Developer Day verwendet wurden:


Weitere Links zum Thema:

Die Printversion dieses Artikels ist in der JavaAktuell 04-2015 erschienen.

JavaOne 2014:
James Gosling, Robots, the Raspberry Pi, and Small Devices [UGF8907] (NetBeans Day)
(James Gosling, Jose Pereda, Shai Almog, Johannes Weigend, Jens Deters)

Debugging and Profiling Robots with James Gosling [CON6699]
(James Gosling, Mark, Heckler, Jose Pereda, Geertjan Wielenga, Jens Deters)

Jens Deters

Etwa 25 Jahre ist es her, dass Jens Deters mit dem Home-Computing begonnen hat. Damals zogen ihn Computer in ihren Bann, und die letzten 15 Jahren hatte er verschiedenste Rollen im IT- und Telekommunikationsumfeld inne (Software Entwickler, Trainer, Berater, Projekt- und Produktmanager). Heute arbeitet er bei codecentric als Senior IT Consultant. Er schreibt regelmäßig über seine Projekte (www.jensd.de, www.mqttfx.org) und trägt zur JavaFX- und IoT-Community bei. Jens ist Mitglied des NetBeans Dream Teams.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.