The Beacons App is a Gluon code sample. For a full list of Gluon code samples, refer to the Gluon website.

In this tutorial, we’ll explain how to create the Beacons application that can be deployed on desktop, Android and iOS devices. It makes use of the Bluetooth Low Energy API from Gluon Attach, to either scan for beacons or broadcast beacons.

Before you start, make sure to check the list of prerequisites for each platform.

Note: This tutorial will use the plugin for IntelliJ, but it works as well on NetBeans and Eclipse.

Code: The code for this project can be found in the samples repository at GitHub. The sample is located under the directory beacons. The reader can clone this repository or create the entire project from scratch, based on the following steps.

BLE and Beacons

Bluetooth Low Energy (BLE), a wireless personal area network technology, while providing the same range of communications as classic bluetooth, considerably reduces the power consumption and cost.

Based on this principle, beacons are small wireless devices designed to transmit a simple radio signal continuously, which can be received by nearby smartphones. They transmit only their ID and their signal power, and the smartphone can read those values and estimate the distance to that device.

Among other uses, one of the possible applications for it is for proximity sensing: physical proximity to certain devices equipped with this technology can be estimated based on their radio receiver’s rssi value. This allows for indoor positioning, indoor behavior tracking, in-store interaction between customers and retailers, indoor navigation, and so on.

iBeacons and other beacon protocols

iBeacon is a brand name created by Apple in 2014, a new location technology that lets any phone using iOS7 or newer constantly scan the environment for BLE devices such as beacons. It is a proprietary, closed standard. Other standards like AltBeacon are open. Google launched Eddystone, its open source beacon, in 2015.

Hardware

If you want to use this technology in production, you will need real beacons equipped with BLE, and implementing one of the mentioned protocols. Those are small devices that can be purchased online from different vendors like Estimote, Kontakt or Gimbal. You can find an extensive list of vendors here.

For development, you can simulate a beacon with different apps on Android and iOS.

Gluon Attach Implementation Details

Under the hood, Attach creates the implementation for accessing native bluetooth services on Android and iOS, so developers don’t need to worry about this.

Beacon Protocol

Beacons transmit small amounts of data. Beacon IDs consists of a maximum of three values: A universally unique identifier (UUID), a major value and a minor value. Every beacon also transmits information about its signal power.

According to the Bluetooth core specification, a beacon advertises a data package called the Scan Response Data. The scan response is divided into what are called AD structures.

Usually, these will be the 31 bytes sent by the beacon on every transmission, corresponding to two AD structures:

Scan Response Data

Android

Android API provides a Bluetooth adapter, and a BLE scanner:

BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
BluetoothLeScanner scanner = adapter.getBluetoothLeScanner();
scanner.startScan(new ScanCallback() {
    @Override
    public void onScanResult(int callbackType, ScanResult result) {
        ScanRecord scanRecord = result.getScanRecord();
        // process scanRecord
    }
});

The resulting ScanRecord object can be processed following the mentioned bytes structure to obtain a ScanDetection instance that can be passed finally to the service callback.

And it also provides a BLE advertiser to broadcast a beacon:

BluetoothLeAdvertiser advertiser = BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser();
advertiser.startAdvertising(parameters, advertiseData, advertiseCallback);

where AdvertiseData is based on the UUID, and major and minor significant values of a given beacon.

ScanDetection detection = processAD(scanRecord.getBytes());
callback.accept(detection);

iOS

iBeacon is closed protocol, but iOS7+ provides a Location framework for dealing with different location services like GPS and beacons. A CLBeaconRegion object defines a type of region that is based on the device’s proximity to a Bluetooth beacon.

While a beacon is in range, the startRangingBeaconsInRegion method is used to begin receiving notifications when the relative distance to the beacon changes.

These notifications will be received through the locationManager:didRangeBeacons:inRegion method, returning an array of CLBeacon with the beacons in range, sorted by proximity. Selecting the first one on that array allows generating a ScanDetection instance that can be passed to the service callback.

It also provides startBroadcast method that takes the UUID, and major and minor significant values of a given beacon to start advertising a beacon.

Creating the project

Please check this tutorial to learn how to create a new Gluon project in IntelliJ.

Select Gluon Mobile - Multiple View Project with Glisten-Afterburner from the list of available Projects. Alternatively, you can also use the Gluon Mobile Maven Archetype to create a new project.

Add the package name (com.gluonhq.samples.beacons) and change the main class name to Beacons.

Add the Beacon and Beacons views, select a JDK 11+ and a valid location for the project.

Finally, the project is created, and the main class is shown:

Beacons class

Modifying the project

Let’s start modifying the default project to create our Beacons application.

First let’s add the Gluon Attach BLE service, to the pom.

pom.xml
<dependencies>
    ...
    <dependency>
        <groupId>com.gluonhq.attach</groupId>
        <artifactId>ble</artifactId>
        <version>${attach.version}</version>
    </dependency>
</dependencies>
...
<plugin>
    <groupId>com.gluonhq</groupId>
    ...
    <configuration>
        ...
        <target>${gluonfx.target}</target>
        <attachList>
            <list>ble</list>
    ...
</plugins>

The FXML files can be edited with Scene Builder, that includes the Gluon Charm dependencies, so you can easily design Gluon Mobile views.

Beacons view

First of all, edit the beacons.fxml file with Scene Builder, remove the content of the VBox, and add a CharmListView control.

Beacons FXML

When a beacon is detected, the BleService will return an instance of ScanDetection that provides the UUID, major and minor id, rssi, and proximity.

Back to the BeaconsPresenter, we will add the CharmListView<ScanDetection, String> that will show all the discovered beacons.

BeaconsPresenter.java
public class BeaconsPresenter extends GluonPresenter<Beacons> {
    ...
    @FXML
    private CharmListView<ScanDetection, String> beaconsList;
}

We need to define a callback that will add each new item found to the list when the service returns a new ScanDetection. Also, if a beacon is already selected, it will update its proximity value whenever it changes.

BeaconsPresenter.java
public class BeaconsPresenter extends GluonPresenter<Beacons> {
    ...

    public void initialize() {
        Consumer<ScanDetection> callback = (ScanDetection t) -> {
            if (t.getUuid().equalsIgnoreCase(settings.getUuid()) && beaconsList.itemsProperty().stream().noneMatch(s ->
                            s.getMajor() == t.getMajor() && s.getMinor() == t.getMinor())) {
                javafx.application.Platform.runLater(() ->
                        beaconsList.itemsProperty().add(t));
            }

            AppViewManager.BEACON_VIEW.getPresenter().ifPresent(p -> {
                BeaconPresenter presenter = (BeaconPresenter) p;
                ScanDetection currentBeacon = presenter.getCurrentBeacon();
                if (currentBeacon != null && currentBeacon.getUuid().equals(t.getUuid()) &&
                        currentBeacon.getMajor() == t.getMajor() &&
                        currentBeacon.getMinor() == t.getMinor()) {
                    presenter.setBeacon(t);
                }
            });
        };
        ...
    }
}

Note that the callback handler is called on a non-JavaFX Thread. Hence, to update user interface components we have to call Platform.runLater().

Then we will define a cell factory that shows, for each beacon found, its UUID, major and minor values, and when selected switches to the Beacon view, passing down the ScanDetection related object.

BeaconsPresenter.java
public class BeaconsPresenter extends GluonPresenter<Beacons> {
    ...
    public void initialize() {
        ...

        beaconsList.setPlaceholder(new Label("No beacons found"));
        beaconsList.setCellFactory(p -> new CharmListCell<>() {

            private ScanDetection scan;
            private final ListTile tile;

            {
                tile = new ListTile();
                tile.setPrimaryGraphic(MaterialDesignIcon.BLUETOOTH.graphic());
                tile.setSecondaryGraphic(MaterialDesignIcon.CHEVRON_RIGHT.graphic());
                tile.setOnMouseClicked(e ->
                        AppViewManager.BEACON_VIEW.switchView().ifPresent(presenter ->
                                ((BeaconPresenter) presenter).setBeacon(scan)));
            }

            @Override
            public void updateItem(ScanDetection item, boolean empty) {
                super.updateItem(item, empty);
                if (item != null && !empty) {
                    scan = item;
                    tile.setTextLine(0, "UUID: " + item.getUuid());
                    tile.setTextLine(1, "Major: " + item.getMajor());
                    tile.setTextLine(2, "Minor: " + item.getMinor());
                    setGraphic(tile);
                } else {
                    setGraphic(null);
                }
            }
        });

    }
}

Finally, we’ll add two buttons to start and stop beacon discovery.

We will use Configuration to set the unique UUID of the group of devices with that same ID that will be scanned.

BeaconsPresenter.java
public class BeaconsPresenter extends GluonPresenter<Beacons> {

    private static final String UUID = "74278BDA-B644-4520-8F0C-720EAF059935";
    ...
    public void initialize() {
        ...
        ObservableList<Button> actions =
                BleService.create()
                        .map(bleService -> {
                            final Button buttonScan = MaterialDesignIcon.BLUETOOTH_SEARCHING.button();
                            final Button buttonStop = MaterialDesignIcon.STOP.button();
                            buttonScan.setOnAction(e -> {
                                bleService.stopScanning();
                                Configuration conf = new Configuration(UUID);
                                bleService.startScanning(conf, callback);
                                buttonStop.setDisable(false);
                            });
                            buttonStop.setOnAction(e -> {
                                bleService.stopScanning();
                                buttonStop.setDisable(true);
                                beaconsList.itemsProperty().clear();
                            });
                            buttonScan.disableProperty().bind(buttonStop.disableProperty().not());
                            buttonStop.setDisable(true);

                            return FXCollections.observableArrayList(buttonScan, buttonStop);
                        })
                        .orElseGet(FXCollections::emptyObservableList);


        beacons.showingProperty().addListener((obs, oldValue, newValue) -> {
            if (newValue) {
                ...
                appBar.getActionItems().addAll(actions);
            }
        });
    }
}

Beacon view

Let’s edit beacon.fxml, removing the content of the VBox, and adding a GridPane, populated with some labels to show the beacon values. Also three circles will inform about its proximity (unknown, far, near or immediate):

Beacon FXML

Once-the fx:id tags have been added to each control, select View→Show sample Controller Skeleton from Scene Builder, copy the content and paste it in the BeaconPresenter.

Beacon skeleton

And we will add the required methods:

BeaconPresenter.java
public class BeaconPresenter extends GluonPresenter<Beacons> {
    ...
    private ScanDetection scanDetection;

    public void initialize() {
        ...
    }

    void setBeacon(ScanDetection scan) {
        scanDetection = scan;
        labelUUID.setText(scan.getUuid());
        labelMajor.setText(String.valueOf(scan.getMajor()));
        labelMinor.setText(String.valueOf(scan.getMinor()));
        labelRSSI.setText(String.valueOf(scan.getRssi()));
        labelDistance.setText(scan.getProximity().name());
    }

    ScanDetection getCurrentBeacon() {
        return beacon.isShowing() ? scanDetection : null;
    }
}

Now let’s bind now the circles' stroke property to the proximity of the detection, adding also a DropShadow effect to the related circle.

BeaconPresenter.java
public class BeaconPresenter extends GluonPresenter<Beacons> {
    ...
    public void initialize() {
        circleFar.setFill(null);
        circleFar.setStroke(Color.TRANSPARENT);
        circleFar.strokeProperty().bind(Bindings.createObjectBinding(() -> {
            if (scanDetection.get() != null && scanDetection.get().getProximity().equals(Proximity.FAR)) {
                circleFar.setEffect(new DropShadow(10, Color.GREEN));
                return Color.GREEN;
            }
            circleFar.setEffect(null);
            return Color.GRAY;
        }, labelDistance.textProperty()));
        circleNear.setFill(null);
        circleNear.setStroke(Color.TRANSPARENT);
        circleNear.strokeProperty().bind(Bindings.createObjectBinding(() -> {
            if (scanDetection.get() != null && scanDetection.get().getProximity().equals(Proximity.NEAR)) {
                circleNear.setEffect(new DropShadow(15, Color.GREEN));
                return Color.GREEN;
            }
            circleNear.setEffect(null);
            return Color.GRAY;
        }, labelDistance.textProperty()));
        circleImmediate.setFill(null);
        circleImmediate.setStroke(Color.TRANSPARENT);
        circleImmediate.strokeProperty().bind(Bindings.createObjectBinding(() -> {
            if (scanDetection.get() != null && scanDetection.get().getProximity().equals(Proximity.IMMEDIATE)) {
                circleImmediate.setEffect(new DropShadow(20, Color.GREEN));
                return Color.GREEN;
            }
            circleImmediate.setEffect(null);
            return Color.GRAY;
        }, labelDistance.textProperty()));
        ...
    }
}

Setting the UUID

Let’s create a Settings class, that will contain the basic settings for this application. For now, just the uuid string that will be scanned. By default we will use this one: "74278BDA-B644-4520-8F0C-720EAF059935".

Settings.java
public class Settings {

    public static final String UUID = "74278BDA-B644-4520-8F0C-720EAF059935";

    private final StringProperty uuid = new SimpleStringProperty(UUID);

    public final StringProperty uuidProperty() {
            return this.uuid;
    }

    public final String getUuid() {
            return this.uuidProperty().get();
    }

    public final void setUuid(final String uuid) {
            this.uuidProperty().set(uuid);
    }
}

The class will be injected in the presenters, so it needs to be added to the reflection list:

pom.xml
<reflectionList>
    <list>com.gluonhq.samples.beacons.views.BeaconPresenter</list>
    <list>com.gluonhq.samples.beacons.views.BeaconsPresenter</list>
    <list>com.gluonhq.samples.beacons.settings.Settings</list>
</reflectionList>

Now we can inject it and replace the UUID string:

BeaconsPresenter.java
public class BeaconsPresenter extends GluonPresenter<Beacons> {

    @Inject
    Settings settings;

    public void initialize() {
        ...
        Configuration conf = new Configuration(settings.getUuid());
        ...
    }
}

Testing the App

Build, run and test the application on desktop, both on HotSpot and creating a native image.

HotSpot

Before creating a native image, let’s run first the app on HotSpot with the regular JDK 11+, as this will be faster faster for spotting and fixing any possible issue.

Select Plugins → javafx → javafx:run from the Maven tool window, or open a terminal and run: mvn javafx:run.

Beacons app on desktop

Native image

Open a terminal and run: mvn gluonfx:build gluonfx:nativerun

If everything works as it should, it is time to target the application for mobile platforms like Android and iOS.

These steps are already documented in the client docs for each IDE. We will refer to the IntelliJ section.

Android

From a Linux machine, run:

mvn -Pandroid gluonfx:build gluonfx:package

When the build finishes, the file target/gluonfx/aarch64-android/gvm/android_project/app/build/intermediates/merged_manifest/debug/out/AndroidManifest.xml already includes the bluetooth required permissions:

AndroidManifest.xml
...
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

Now, plug an Android device and run:

mvn -Pandroid gluonfx:package gluonfx:install gluonfx:nativerun

When running the application, notice that if bluetooth is not enabled, a popup will ask you to enable it.

Providing there is a beacon transmitter nearby, it will be discovered:

Beacons app on Android

Selecting that beacon, it will show its proximity:

Beacon found on Android
iOS

On iOS, the Default-Info.plist file has to include the some related Bluetooth keys to require location services. You can change its description.

On MacOS run:

mvn -Pios gluonfx:build

When the build finishes, copy the target/gluonfx/arm64-ios/gensrc/ios/Default-Info.plist file into src/ios/Default-Info.plist, edit it and add:

Default-Info.plist
    ...
    <key>NSLocationWhenInUseUsageDescription</key>
    <string>This app needs location services</string>
    <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
    <string>This app needs location services</string>
    <key>NSBluetoothAlwaysUsageDescription</key>
    <string>This app needs bluetooth services</string>
</dict>
</plist>

Now plug an iOS device and run:

mvn -Pios gluonfx:link gluonfx:nativerun

When running the application, notice that if bluetooth is not enabled, a popup will ask you to enable it, and the first time you click on the start scanning button, a popup will ask you to enable location services. This can be changed in Settings → Security → Location.

Providing there is a beacon transmitter nearby, it will be discovered:

Beacons app on iOS

Selecting that beacon, it will show its proximity:

Beacon found on iOS

Beacon advertising and Settings

Adding the Beacon advertising functionality and a settings view to set different UUID values for scanning or advertising, or different minor, major values for advertising, are left out of this tutorial.

The reader can have a look at the full code for this project in the samples repository at GitHub.

The project includes a home view, to select either scanning or advertising:

Home View

The advertising view:

Advertising View

And the settings view:

Settings View

Conclusion

Throughout this post we’ve covered in detail the basic steps to use the Bluetooth Low Energy API within a Multi View project, and we have tested the application on desktop and mobile devices, by building a native image.

If you have made it this far, congratulations, we hope this sample, and the documentation was helpful to you! In case you have any questions, your first stop should be the Gluon support page, where you can access the latest documentation. Gluon also recommends the community to support each other over at the Gluon StackOverflow page. Finally, Gluon offers commercial support as well, to kick start your projects.